import { existsSync , readFileSync } from 'fs' ; import * as stripJsonComments from 'strip-json-comments' ; import * as _ from 'lodash' ; import { join } from 'path' ; import { ChromeDebugAdapter , IRestartRequestArgs , ISetBreakpointsArgs , ISetBreakpointsResponseBody , ITelemetryPropertyCollector , } from 'vscode-chrome-debug-core' ; import { Event , logger , TerminatedEvent } from 'vscode-debugadapter' ; import { DebugProtocol } from 'vscode-debugprotocol' ; import * as extProtocol from '../common/extensionProtocol' ; import { NativeScriptSourceMapTransformer } from './nativeScriptSourceMapTransformer' ; const reconnectAfterLiveSyncTimeout = 10 * 1000 ; export class NativeScriptDebugAdapter extends ChromeDebugAdapter { private _idCounter = 0 ; private _pendingRequests : object = { } ; private isLiveSync : boolean = false ; private portWaitingResolve : any ; private isDisconnecting : boolean = false ; private isLiveSyncRestart : boolean = false ; private breakPointsCache : { [ file ] : { args : ISetBreakpointsArgs ; requestSeq : number ; ids : number [ ] ; } ; } = { } ; public attach ( args ) { return this . processRequestAndAttach ( args ) ; } public launch ( args ) { return this . processRequestAndAttach ( args ) ; } public onPortReceived ( port ) { this . portWaitingResolve && this . portWaitingResolve ( port ) ; } public onExtensionResponse ( response ) { this . _pendingRequests [ response . requestId ] ( response . result ) ; delete this . _pendingRequests [ response . requestId ] ; } public disconnect ( args ) { this . isDisconnecting = true ; if ( ! this . isLiveSyncRestart ) { this . callRemoteMethod ( 'buildService' , 'disconnect' ) ; } super . disconnect ( args ) ; } public async setCachedBreakpointsForScript ( script ) { const breakPointData = this . breakPointsCache [ script ] ; if ( breakPointData ) { await this . setBreakpoints ( breakPointData . args , null , breakPointData . requestSeq , breakPointData . ids ) ; } } public async setBreakpoints ( args , telemetryPropertyCollector , requestSeq , ids ? ) { if ( args && args . source && args . source . path ) { this . breakPointsCache [ args . source . path ] = { args : _ . cloneDeep ( args ) , requestSeq , ids } ; } return super . setBreakpoints ( args , telemetryPropertyCollector , requestSeq , ids ) ; } protected async terminateSession ( reason , disconnectArgs ? , restart ? ) { let restartRequestArgs = restart ; let timeoutId ; if ( ! this . isDisconnecting && this . isLiveSync ) { const portProm = new Promise < any > ( ( res , rej ) => { this . portWaitingResolve = res ; timeoutId = setTimeout ( ( ) => { res ( ) ; } , reconnectAfterLiveSyncTimeout ) ; } ) ; restartRequestArgs = await portProm ; this . isLiveSyncRestart = restartRequestArgs && ! ! restartRequestArgs . port ; clearTimeout ( timeoutId ) ; } await super . terminateSession ( reason , disconnectArgs , restartRequestArgs ) ; } protected hookConnectionEvents ( ) { super . hookConnectionEvents ( ) ; this . chrome . Log . on ( 'entryAdded' , ( params ) => this . onEntryAdded ( params ) ) ; } protected onEntryAdded ( params ) { if ( params && params . entry && params . entry . level ) { const message = params . entry ; message . type = message . level ; const that = this as any ; const script = that . getScriptByUrl && that . getScriptByUrl ( params . entry . url ) ; if ( script ) { message . stack = { callFrames : [ { columnNumber : 0 , lineNumber : params . entry . lineNumber , scriptId : script . scriptId , url : params . entry . url , } , ] , } ; } this . onMessageAdded ( { message , } ) ; } } private async processRequestAndAttach ( args ) { const transformedArgs = this . translateArgs ( args ) ; if ( args . __restart ) { transformedArgs . port = args . __restart . port ; } else { this . _session . sendEvent ( new Event ( extProtocol . BEFORE_DEBUG_START ) ) ; transformedArgs . port = await this . callRemoteMethod ( 'buildService' , 'processRequest' , transformedArgs ) ; } if ( ! transformedArgs . port ) { this . _session . sendEvent ( new TerminatedEvent ( ) ) ; } const appDirPath = this . getAppDirPath ( transformedArgs . webRoot ) ; ( this . pathTransformer as any ) . setTransformOptions ( args . platform , appDirPath , transformedArgs . webRoot ) ; ( this . sourceMapTransformer as NativeScriptSourceMapTransformer ) . setTransformOptions ( args . platform . toLowerCase ( ) , this ) ; ( ChromeDebugAdapter as any ) . SET_BREAKPOINTS_TIMEOUT = 20000 ; this . isLiveSync = args . watch ; return super . attach ( transformedArgs ) ; } private getAppDirPath ( webRoot ) { const pathToNsconfig = join ( webRoot , 'nsconfig.json' ) ; const pathToNativeScriptConfig = join ( webRoot , 'nativescript.config.ts' ) ; if ( existsSync ( pathToNsconfig ) ) { try { const content = readFileSync ( pathToNsconfig ) . toString ( ) ; const jsonContent = JSON . parse ( stripJsonComments ( content ) ) ; return jsonContent . appPath ; } catch ( err ) { } } else if ( existsSync ( pathToNativeScriptConfig ) ) { try { const nativeScriptConfigTSContent = readFileSync ( pathToNativeScriptConfig , { encoding : 'UTF-8' , } ) ; if ( nativeScriptConfigTSContent ) { if ( nativeScriptConfigTSContent . indexOf ( `appPath: 'src'` ) === - 1 ) { const appPath = nativeScriptConfigTSContent . split ( /appPath:\s*["'`](\w+)["'`]/gim ) [ 1 ] ; return appPath ; } } } catch ( err ) { } } } param webRoot private isAngularProject ( webRoot ) { let isAngularProject = existsSync ( join ( webRoot , 'angular.json' ) ) ; if ( isAngularProject ) { logger . log ( 'Angular project detected, found angular.json file.' ) ; } else { let packageJson = existsSync ( join ( webRoot , 'package.json' ) ) ; if ( packageJson ) { try { const packageJsonContent = readFileSync ( join ( webRoot , 'package.json' ) , { encoding : 'UTF-8' , } ) ; const jsonContent = JSON . parse ( stripJsonComments ( packageJsonContent ) ) ; if ( jsonContent && jsonContent . dependencies && jsonContent . dependencies [ '@angular/core' ] ) { isAngularProject = true ; } } catch ( err ) { } } } return isAngularProject ; } private translateArgs ( args ) { if ( args . diagnosticLogging ) { args . trace = args . diagnosticLogging ; } if ( args . appRoot ) { args . webRoot = args . appRoot ; } if ( ! args . sourceMapPathOverrides ) { args . sourceMapPathOverrides = { } ; } const appDirPath = this . getAppDirPath ( args . webRoot ) || ( this . isAngularProject ( args . webRoot ) ? 'src' : 'app' ) ; const fullAppDirPath = join ( args . webRoot , appDirPath ) ; if ( ! args . sourceMapPathOverrides [ 'webpack:///*' ] ) { args . sourceMapPathOverrides [ 'webpack:///*' ] = `${ fullAppDirPath }/*` ; } const webpackConfigFile = join ( args . webRoot , 'webpack.config.js' ) ; if ( existsSync ( webpackConfigFile ) ) { try { const webpackConfig = require ( webpackConfigFile ) ; const platform = args . platform && args . platform . toLowerCase ( ) ; const config = webpackConfig ( { [ `${ platform }` ] : platform } ) ; if ( config && config . output && config . output . library ) { const sourceMapPathOverrideWithLib = `webpack://${ config . output . library }/*` ; args . sourceMapPathOverrides [ sourceMapPathOverrideWithLib ] = args . sourceMapPathOverrides [ sourceMapPathOverrideWithLib ] || `${ fullAppDirPath }/*` ; } } catch ( err ) { logger . warn ( `Error when trying to require webpack.config.js file from path '${ webpackConfigFile }'. Error is: ${ err }` ) ; } } return args ; } private callRemoteMethod < T > ( service , method , ... args ) { const request = { id : `req${ ++ this . _idCounter }` , service , method , args } ; return new Promise < T > ( ( res , rej ) => { this . _pendingRequests [ request . id ] = res ; this . _session . sendEvent ( new Event ( extProtocol . NS_DEBUG_ADAPTER_MESSAGE , request ) ) ; } ) ; } }