Skip to content

Commit f6ab301

Browse files
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.
1 parent b11ae09 commit f6ab301

7 files changed

+93
-17
lines changed

Diff for: package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
"semver": "5.6.0",
3030
"universal-analytics": "0.4.15",
3131
"uuid": "3.3.2",
32-
"vscode-chrome-debug-core": "6.7.45",
32+
"vscode-chrome-debug-core": "6.7.46",
3333
"vscode-debugadapter": "1.34.0"
3434
},
3535
"devDependencies": {

Diff for: src/debug-adapter/nativeScriptDebug.ts

+2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import * as path from 'path';
33
import { chromeConnection, ChromeDebugSession } from 'vscode-chrome-debug-core';
44
import { NativeScriptDebugAdapter } from './nativeScriptDebugAdapter';
55
import { NativeScriptPathTransformer } from './nativeScriptPathTransformer';
6+
import { NativeScriptSourceMapTransformer } from './nativeScriptSourceMapTransformer';
67
import { NativeScriptTargetDiscovery } from './nativeScriptTargetDiscovery';
78

89
class NSAndroidConnection extends chromeConnection.ChromeConnection {
@@ -18,4 +19,5 @@ ChromeDebugSession.run(ChromeDebugSession.getSession(
1819
extensionName: 'nativescript-extension',
1920
logFilePath: path.join(os.tmpdir(), 'nativescript-extension.txt'),
2021
pathTransformer: NativeScriptPathTransformer,
22+
sourceMapTransformer: NativeScriptSourceMapTransformer,
2123
}));

Diff for: src/debug-adapter/nativeScriptDebugAdapter.ts

+33-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,17 @@
11
import { existsSync, readFileSync } from 'fs';
2+
import * as _ from 'lodash';
23
import { join } from 'path';
3-
import { ChromeDebugAdapter, IRestartRequestArgs } from 'vscode-chrome-debug-core';
4+
import {
5+
ChromeDebugAdapter,
6+
IRestartRequestArgs,
7+
ISetBreakpointsArgs,
8+
ISetBreakpointsResponseBody,
9+
ITelemetryPropertyCollector,
10+
} from 'vscode-chrome-debug-core';
411
import { Event, TerminatedEvent } from 'vscode-debugadapter';
512
import { DebugProtocol } from 'vscode-debugprotocol';
613
import * as extProtocol from '../common/extensionProtocol';
14+
import { NativeScriptSourceMapTransformer } from './nativeScriptSourceMapTransformer';
715

816
const reconnectAfterLiveSyncTimeout = 10 * 1000;
917

@@ -14,6 +22,7 @@ export class NativeScriptDebugAdapter extends ChromeDebugAdapter {
1422
private portWaitingResolve: any;
1523
private isDisconnecting: boolean = false;
1624
private isLiveSyncRestart: boolean = false;
25+
private breakPointsCache: { [file: string]: { args: ISetBreakpointsArgs, requestSeq: number, ids: number[] } } = {};
1726

1827
public attach(args: any): Promise<void> {
1928
return this.processRequestAndAttach(args);
@@ -41,6 +50,27 @@ export class NativeScriptDebugAdapter extends ChromeDebugAdapter {
4150
super.disconnect(args);
4251
}
4352

53+
public async setCachedBreakpointsForScript(script: string): Promise<void> {
54+
const breakPointData = this.breakPointsCache[script];
55+
56+
if (breakPointData) {
57+
await this.setBreakpoints(breakPointData.args, null, breakPointData.requestSeq, breakPointData.ids);
58+
}
59+
}
60+
61+
public async setBreakpoints(
62+
args: ISetBreakpointsArgs,
63+
telemetryPropertyCollector: ITelemetryPropertyCollector,
64+
requestSeq: number,
65+
ids?: number[]): Promise<ISetBreakpointsResponseBody> {
66+
67+
if (args && args.source && args.source.path) {
68+
this.breakPointsCache[args.source.path] = { args: _.cloneDeep(args), requestSeq, ids };
69+
}
70+
71+
return super.setBreakpoints(args, telemetryPropertyCollector, requestSeq, ids);
72+
}
73+
4474
protected async terminateSession(reason: string, disconnectArgs?: DebugProtocol.DisconnectArguments, restart?: IRestartRequestArgs): Promise<void> {
4575
let restartRequestArgs = restart;
4676
let timeoutId;
@@ -113,6 +143,7 @@ export class NativeScriptDebugAdapter extends ChromeDebugAdapter {
113143
const appDirPath = this.getAppDirPath(transformedArgs.webRoot);
114144

115145
(this.pathTransformer as any).setTransformOptions(args.platform, appDirPath, transformedArgs.webRoot);
146+
(this.sourceMapTransformer as NativeScriptSourceMapTransformer).setDebugAdapter(this);
116147
(ChromeDebugAdapter as any).SET_BREAKPOINTS_TIMEOUT = 20000;
117148

118149
this.isLiveSync = args.watch;
@@ -145,7 +176,7 @@ export class NativeScriptDebugAdapter extends ChromeDebugAdapter {
145176
}
146177

147178
if (!args.sourceMapPathOverrides) {
148-
args.sourceMapPathOverrides = { };
179+
args.sourceMapPathOverrides = {};
149180
}
150181

151182
if (!args.sourceMapPathOverrides['webpack:///*']) {

Diff for: src/debug-adapter/nativeScriptPathTransformer.ts

+15-7
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,14 @@ export class NativeScriptPathTransformer extends UrlPathTransformer {
2525
}
2626

2727
const isAndroid = this.targetPlatform === 'android';
28+
const isIOS = this.targetPlatform === 'ios';
2829

2930
if (_.startsWith(scriptUrl, 'mdha:')) {
3031
scriptUrl = _.trimStart(scriptUrl, 'mdha:');
3132
}
3233

3334
if (path.isAbsolute(scriptUrl) && fs.existsSync(scriptUrl)) {
34-
return Promise.resolve(scriptUrl);
35+
return scriptUrl;
3536
}
3637

3738
const filePattern = this.filePatterns[this.targetPlatform];
@@ -56,20 +57,23 @@ export class NativeScriptPathTransformer extends UrlPathTransformer {
5657
let platformSpecificPath = this.getPlatformSpecificPath(absolutePath);
5758

5859
if (platformSpecificPath) {
59-
return Promise.resolve(platformSpecificPath);
60+
return platformSpecificPath;
6061
}
6162

6263
if (isAndroid) {
6364
// handle files like /data/data/internal/ts_helpers.ts
6465
absolutePath = path.resolve(path.join(this.webRoot, 'platforms', this.targetPlatform.toLowerCase(), 'app', 'src', 'main', 'assets', relativePath));
65-
platformSpecificPath = this.getPlatformSpecificPath(absolutePath);
66+
} else if (isIOS) {
67+
absolutePath = path.resolve(path.join(this.webRoot, 'platforms', this.targetPlatform.toLowerCase(), this.getAppName(this.webRoot), relativePath));
68+
}
6669

67-
if (platformSpecificPath) {
68-
return Promise.resolve(platformSpecificPath);
69-
}
70+
platformSpecificPath = this.getPlatformSpecificPath(absolutePath);
71+
72+
if (platformSpecificPath) {
73+
return platformSpecificPath;
7074
}
7175

72-
return Promise.resolve(scriptUrl);
76+
return scriptUrl;
7377
}
7478

7579
private getPlatformSpecificPath(rawPath: string): string {
@@ -89,4 +93,8 @@ export class NativeScriptPathTransformer extends UrlPathTransformer {
8993

9094
return null;
9195
}
96+
97+
private getAppName(projectDir: string): string {
98+
return _.filter(projectDir.split(''), (c) => /[a-zA-Z0-9]/.test(c)).join('');
99+
}
92100
}
+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { BaseSourceMapTransformer } from 'vscode-chrome-debug-core';
2+
import { NativeScriptDebugAdapter } from './nativeScriptDebugAdapter';
3+
4+
export class NativeScriptSourceMapTransformer extends BaseSourceMapTransformer {
5+
private debugAdapter: NativeScriptDebugAdapter;
6+
7+
constructor(sourceHandles: any) {
8+
super(sourceHandles);
9+
}
10+
11+
public setDebugAdapter(debugAdapter: NativeScriptDebugAdapter): void {
12+
this.debugAdapter = debugAdapter;
13+
}
14+
15+
public async scriptParsed(pathToGenerated: string, sourceMapURL: string): Promise<string[]> {
16+
const scriptParsedResult = await super.scriptParsed(pathToGenerated, sourceMapURL);
17+
18+
if (scriptParsedResult && scriptParsedResult.length) {
19+
for (const script of scriptParsedResult) {
20+
await this.debugAdapter.setCachedBreakpointsForScript(script);
21+
}
22+
}
23+
24+
return scriptParsedResult;
25+
}
26+
}

Diff for: src/main.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -110,8 +110,8 @@ export function activate(context: vscode.ExtensionContext) {
110110
const method = service[request.method];
111111
const response = typeof method === 'function' ? service[request.method].call(service, ...request.args) : method;
112112

113-
if (response.then) {
114-
response.then((result) => event.session.customRequest('onExtensionResponse', { requestId: request.id, result }),
113+
if (response && response.then) {
114+
response.then((result) => event.session && event.session.customRequest('onExtensionResponse', { requestId: request.id, result }),
115115
(err: Error) => {
116116
vscode.window.showErrorMessage(err.message);
117117
event.session.customRequest('onExtensionResponse', { requestId: request.id, isError: true, message: err.message });

Diff for: src/tests/nativeScriptDebugAdapter.tests.ts

+14-5
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ const defaultArgsMock: any = {
2222
platform: 'android',
2323
request: 'attach',
2424
stopOnEntry: true,
25-
tnsArgs: [ 'mockArgs'],
25+
tnsArgs: ['mockArgs'],
2626
watch: true,
2727
};
2828

@@ -37,6 +37,7 @@ describe('NativeScriptDebugAdapter', () => {
3737
let chromeSessionMock: any;
3838
let chromeConnectionMock: any;
3939
let pathTransformerMock: any;
40+
let sourceMapTransformer: any;
4041

4142
beforeEach(() => {
4243
chromeSessionMock = {
@@ -61,16 +62,24 @@ describe('NativeScriptDebugAdapter', () => {
6162
setTransformOptions: () => ({}),
6263
};
6364

64-
nativeScriptDebugAdapter = new NativeScriptDebugAdapter(
65-
{ chromeConnection: mockConstructor(chromeConnectionMock), pathTransformer: mockConstructor(pathTransformerMock) },
65+
sourceMapTransformer = {
66+
clearTargetContext: () => undefined,
67+
setDebugAdapter: () => undefined,
68+
};
69+
70+
nativeScriptDebugAdapter = new NativeScriptDebugAdapter({
71+
chromeConnection: mockConstructor(chromeConnectionMock),
72+
pathTransformer: mockConstructor(pathTransformerMock),
73+
sourceMapTransformer: mockConstructor(sourceMapTransformer),
74+
},
6675
chromeSessionMock,
6776
);
6877

6978
ChromeDebugAdapter.prototype.attach = () => Promise.resolve();
7079
});
7180

72-
const platforms = [ 'android', 'ios' ];
73-
const launchMethods = [ 'launch', 'attach' ];
81+
const platforms = ['android', 'ios'];
82+
const launchMethods = ['launch', 'attach'];
7483

7584
platforms.forEach((platform) => {
7685
launchMethods.forEach((method) => {

0 commit comments

Comments
 (0)