From f6ab3011bea4ceacb982c9a83f1eb245f958b03f Mon Sep 17 00:00:00 2001 From: rosen-vladimirov Date: Wed, 13 Mar 2019 18:14:40 +0200 Subject: [PATCH] feat: add support for debug with hmr Add support for Debug + HMR - currently it is not working, as when a hot-module is applied, VSCode does not know that the current hot-module is mapped to the actual changed file (main-view-model) for example. To fix this, add SourceMapTransformer and plug in the scriptParsed method. When breakpoint is set, cache it (in nativeScriptDebugAdapter), so once hot-module is applied, set the breakpoints from its original file (i.e. main-view-model) in the newly applied hot module. This is exactly how Chrome works. Also, add logic in pathTransformer to map files from `platforms` dir when debugging on iOS. --- package.json | 2 +- src/debug-adapter/nativeScriptDebug.ts | 2 ++ src/debug-adapter/nativeScriptDebugAdapter.ts | 35 +++++++++++++++++-- .../nativeScriptPathTransformer.ts | 22 ++++++++---- .../nativeScriptSourceMapTransformer.ts | 26 ++++++++++++++ src/main.ts | 4 +-- src/tests/nativeScriptDebugAdapter.tests.ts | 19 +++++++--- 7 files changed, 93 insertions(+), 17 deletions(-) create mode 100644 src/debug-adapter/nativeScriptSourceMapTransformer.ts diff --git a/package.json b/package.json index 1b91a83..7a403f4 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "semver": "5.6.0", "universal-analytics": "0.4.15", "uuid": "3.3.2", - "vscode-chrome-debug-core": "6.7.45", + "vscode-chrome-debug-core": "6.7.46", "vscode-debugadapter": "1.34.0" }, "devDependencies": { diff --git a/src/debug-adapter/nativeScriptDebug.ts b/src/debug-adapter/nativeScriptDebug.ts index 20e02fe..b7aa793 100644 --- a/src/debug-adapter/nativeScriptDebug.ts +++ b/src/debug-adapter/nativeScriptDebug.ts @@ -3,6 +3,7 @@ import * as path from 'path'; import { chromeConnection, ChromeDebugSession } from 'vscode-chrome-debug-core'; import { NativeScriptDebugAdapter } from './nativeScriptDebugAdapter'; import { NativeScriptPathTransformer } from './nativeScriptPathTransformer'; +import { NativeScriptSourceMapTransformer } from './nativeScriptSourceMapTransformer'; import { NativeScriptTargetDiscovery } from './nativeScriptTargetDiscovery'; class NSAndroidConnection extends chromeConnection.ChromeConnection { @@ -18,4 +19,5 @@ ChromeDebugSession.run(ChromeDebugSession.getSession( extensionName: 'nativescript-extension', logFilePath: path.join(os.tmpdir(), 'nativescript-extension.txt'), pathTransformer: NativeScriptPathTransformer, + sourceMapTransformer: NativeScriptSourceMapTransformer, })); diff --git a/src/debug-adapter/nativeScriptDebugAdapter.ts b/src/debug-adapter/nativeScriptDebugAdapter.ts index 19724b6..49075ee 100644 --- a/src/debug-adapter/nativeScriptDebugAdapter.ts +++ b/src/debug-adapter/nativeScriptDebugAdapter.ts @@ -1,9 +1,17 @@ import { existsSync, readFileSync } from 'fs'; +import * as _ from 'lodash'; import { join } from 'path'; -import { ChromeDebugAdapter, IRestartRequestArgs } from 'vscode-chrome-debug-core'; +import { + ChromeDebugAdapter, + IRestartRequestArgs, + ISetBreakpointsArgs, + ISetBreakpointsResponseBody, + ITelemetryPropertyCollector, +} from 'vscode-chrome-debug-core'; import { Event, TerminatedEvent } from 'vscode-debugadapter'; import { DebugProtocol } from 'vscode-debugprotocol'; import * as extProtocol from '../common/extensionProtocol'; +import { NativeScriptSourceMapTransformer } from './nativeScriptSourceMapTransformer'; const reconnectAfterLiveSyncTimeout = 10 * 1000; @@ -14,6 +22,7 @@ export class NativeScriptDebugAdapter extends ChromeDebugAdapter { private portWaitingResolve: any; private isDisconnecting: boolean = false; private isLiveSyncRestart: boolean = false; + private breakPointsCache: { [file: string]: { args: ISetBreakpointsArgs, requestSeq: number, ids: number[] } } = {}; public attach(args: any): Promise { return this.processRequestAndAttach(args); @@ -41,6 +50,27 @@ export class NativeScriptDebugAdapter extends ChromeDebugAdapter { super.disconnect(args); } + public async setCachedBreakpointsForScript(script: string): Promise { + const breakPointData = this.breakPointsCache[script]; + + if (breakPointData) { + await this.setBreakpoints(breakPointData.args, null, breakPointData.requestSeq, breakPointData.ids); + } + } + + public async setBreakpoints( + args: ISetBreakpointsArgs, + telemetryPropertyCollector: ITelemetryPropertyCollector, + requestSeq: number, + ids?: number[]): Promise { + + 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: string, disconnectArgs?: DebugProtocol.DisconnectArguments, restart?: IRestartRequestArgs): Promise { let restartRequestArgs = restart; let timeoutId; @@ -113,6 +143,7 @@ export class NativeScriptDebugAdapter extends ChromeDebugAdapter { const appDirPath = this.getAppDirPath(transformedArgs.webRoot); (this.pathTransformer as any).setTransformOptions(args.platform, appDirPath, transformedArgs.webRoot); + (this.sourceMapTransformer as NativeScriptSourceMapTransformer).setDebugAdapter(this); (ChromeDebugAdapter as any).SET_BREAKPOINTS_TIMEOUT = 20000; this.isLiveSync = args.watch; @@ -145,7 +176,7 @@ export class NativeScriptDebugAdapter extends ChromeDebugAdapter { } if (!args.sourceMapPathOverrides) { - args.sourceMapPathOverrides = { }; + args.sourceMapPathOverrides = {}; } if (!args.sourceMapPathOverrides['webpack:///*']) { diff --git a/src/debug-adapter/nativeScriptPathTransformer.ts b/src/debug-adapter/nativeScriptPathTransformer.ts index 1799cea..74aa1e7 100644 --- a/src/debug-adapter/nativeScriptPathTransformer.ts +++ b/src/debug-adapter/nativeScriptPathTransformer.ts @@ -25,13 +25,14 @@ export class NativeScriptPathTransformer extends UrlPathTransformer { } const isAndroid = this.targetPlatform === 'android'; + const isIOS = this.targetPlatform === 'ios'; if (_.startsWith(scriptUrl, 'mdha:')) { scriptUrl = _.trimStart(scriptUrl, 'mdha:'); } if (path.isAbsolute(scriptUrl) && fs.existsSync(scriptUrl)) { - return Promise.resolve(scriptUrl); + return scriptUrl; } const filePattern = this.filePatterns[this.targetPlatform]; @@ -56,20 +57,23 @@ export class NativeScriptPathTransformer extends UrlPathTransformer { let platformSpecificPath = this.getPlatformSpecificPath(absolutePath); if (platformSpecificPath) { - return Promise.resolve(platformSpecificPath); + return platformSpecificPath; } if (isAndroid) { // handle files like /data/data/internal/ts_helpers.ts absolutePath = path.resolve(path.join(this.webRoot, 'platforms', this.targetPlatform.toLowerCase(), 'app', 'src', 'main', 'assets', relativePath)); - platformSpecificPath = this.getPlatformSpecificPath(absolutePath); + } else if (isIOS) { + absolutePath = path.resolve(path.join(this.webRoot, 'platforms', this.targetPlatform.toLowerCase(), this.getAppName(this.webRoot), relativePath)); + } - if (platformSpecificPath) { - return Promise.resolve(platformSpecificPath); - } + platformSpecificPath = this.getPlatformSpecificPath(absolutePath); + + if (platformSpecificPath) { + return platformSpecificPath; } - return Promise.resolve(scriptUrl); + return scriptUrl; } private getPlatformSpecificPath(rawPath: string): string { @@ -89,4 +93,8 @@ export class NativeScriptPathTransformer extends UrlPathTransformer { return null; } + + private getAppName(projectDir: string): string { + return _.filter(projectDir.split(''), (c) => /[a-zA-Z0-9]/.test(c)).join(''); + } } diff --git a/src/debug-adapter/nativeScriptSourceMapTransformer.ts b/src/debug-adapter/nativeScriptSourceMapTransformer.ts new file mode 100644 index 0000000..a5ccfae --- /dev/null +++ b/src/debug-adapter/nativeScriptSourceMapTransformer.ts @@ -0,0 +1,26 @@ +import { BaseSourceMapTransformer } from 'vscode-chrome-debug-core'; +import { NativeScriptDebugAdapter } from './nativeScriptDebugAdapter'; + +export class NativeScriptSourceMapTransformer extends BaseSourceMapTransformer { + private debugAdapter: NativeScriptDebugAdapter; + + constructor(sourceHandles: any) { + super(sourceHandles); + } + + public setDebugAdapter(debugAdapter: NativeScriptDebugAdapter): void { + this.debugAdapter = debugAdapter; + } + + public async scriptParsed(pathToGenerated: string, sourceMapURL: string): Promise { + const scriptParsedResult = await super.scriptParsed(pathToGenerated, sourceMapURL); + + if (scriptParsedResult && scriptParsedResult.length) { + for (const script of scriptParsedResult) { + await this.debugAdapter.setCachedBreakpointsForScript(script); + } + } + + return scriptParsedResult; + } +} diff --git a/src/main.ts b/src/main.ts index 620cad9..43c7135 100644 --- a/src/main.ts +++ b/src/main.ts @@ -110,8 +110,8 @@ export function activate(context: vscode.ExtensionContext) { const method = service[request.method]; const response = typeof method === 'function' ? service[request.method].call(service, ...request.args) : method; - if (response.then) { - response.then((result) => event.session.customRequest('onExtensionResponse', { requestId: request.id, result }), + if (response && response.then) { + response.then((result) => event.session && event.session.customRequest('onExtensionResponse', { requestId: request.id, result }), (err: Error) => { vscode.window.showErrorMessage(err.message); event.session.customRequest('onExtensionResponse', { requestId: request.id, isError: true, message: err.message }); diff --git a/src/tests/nativeScriptDebugAdapter.tests.ts b/src/tests/nativeScriptDebugAdapter.tests.ts index 49c7340..cca5124 100644 --- a/src/tests/nativeScriptDebugAdapter.tests.ts +++ b/src/tests/nativeScriptDebugAdapter.tests.ts @@ -22,7 +22,7 @@ const defaultArgsMock: any = { platform: 'android', request: 'attach', stopOnEntry: true, - tnsArgs: [ 'mockArgs'], + tnsArgs: ['mockArgs'], watch: true, }; @@ -37,6 +37,7 @@ describe('NativeScriptDebugAdapter', () => { let chromeSessionMock: any; let chromeConnectionMock: any; let pathTransformerMock: any; + let sourceMapTransformer: any; beforeEach(() => { chromeSessionMock = { @@ -61,16 +62,24 @@ describe('NativeScriptDebugAdapter', () => { setTransformOptions: () => ({}), }; - nativeScriptDebugAdapter = new NativeScriptDebugAdapter( - { chromeConnection: mockConstructor(chromeConnectionMock), pathTransformer: mockConstructor(pathTransformerMock) }, + sourceMapTransformer = { + clearTargetContext: () => undefined, + setDebugAdapter: () => undefined, + }; + + nativeScriptDebugAdapter = new NativeScriptDebugAdapter({ + chromeConnection: mockConstructor(chromeConnectionMock), + pathTransformer: mockConstructor(pathTransformerMock), + sourceMapTransformer: mockConstructor(sourceMapTransformer), + }, chromeSessionMock, ); ChromeDebugAdapter.prototype.attach = () => Promise.resolve(); }); - const platforms = [ 'android', 'ios' ]; - const launchMethods = [ 'launch', 'attach' ]; + const platforms = ['android', 'ios']; + const launchMethods = ['launch', 'attach']; platforms.forEach((platform) => { launchMethods.forEach((method) => {