Skip to content

Commit 1023dc7

Browse files
Fix misaligned lines in debugger (#634)
This PR fixes a problem with debugger on expo projects, caused by prelude lines added to the entry bundle file added by expo. The solution is t0 add the required offset to the prelude causing the problem and scanning for it when receiving source map in the debuger. It is possible that the changes will no longer be needed in future versions of react native as the expo team tries to correct source map generation to include extra lines, for more information read those PRs: - [expo side ](expo/expo#29463) - [metro side](facebook/metro#1284) This is why we add version check before adding lineOffset to initial source map. ### How Has This Been Tested: - open expo project and set a breakpoint, before the changes it would trigger 2 lines above the place it's been set. - save any change in the file containing breakpoint and make sure it still works. - check if links to files generated next to console logs are pointing in to the correct place, again both before and after making changes to the file. - check if error position indicator (when uncaught exception is raised) points to the correct position. ### Additional changes: This PR also fixes a minor mistake with `getReactNativeVersion` utility that does not need to be async and as it is used, we make it synchronous as part of this PR. --------- Co-authored-by: Krzysztof Magiera <[email protected]>
1 parent 6544ae4 commit 1023dc7

File tree

7 files changed

+59
-19
lines changed

7 files changed

+59
-19
lines changed

packages/vscode-extension/lib/metro_helpers.js

+12-8
Original file line numberDiff line numberDiff line change
@@ -42,15 +42,19 @@ function adaptMetroConfig(config) {
4242
}
4343
} else if (module.path === "__env__") {
4444
// this handles @expo/env plugin, which is used to inject environment variables
45-
// the code below instantiates a global variable __EXPO_ENV_PRELUDE_LINES__ that stores
46-
// the number of lines in the prelude. This is used to calculate the line number offset
47-
// when reporting line numbers from the JS runtime. The reason why this is needed, is that
45+
// the code below exposes the number of lines in the prelude.
46+
// This is used to calculate the line number offset
47+
// when reporting line numbers from the JS runtime, breakpoints
48+
// and uncaught exceptions. The reason why this is needed, is that
4849
// metro doesn't include __env__ prelude in the source map resulting in the source map
49-
// transformation getting shifted by the number of lines in the prelude.
50-
const expoEnvCode = module.output[0].data.code;
51-
if (!expoEnvCode.includes("__EXPO_ENV_PRELUDE_LINES__")) {
52-
module.output[0].data.code = `${expoEnvCode};var __EXPO_ENV_PRELUDE_LINES__=${module.output[0].data.lineCount};`;
53-
}
50+
// transformation getting shifted by the number of lines in the expo generated prelude.
51+
process.stdout.write(
52+
JSON.stringify({
53+
type: "RNIDE_expo_env_prelude_lines",
54+
lineCount: module.output[0].data.lineCount,
55+
})
56+
);
57+
process.stdout.write("\n");
5458
}
5559
return origProcessModuleFilter(module);
5660
};

packages/vscode-extension/lib/runtime.js

+1-2
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,7 @@ function wrapConsole(consoleFunc) {
3030
const stack = parseErrorStack(new Error().stack);
3131
const expoLogIndex = stack.findIndex((frame) => frame.methodName === "__expoConsoleLog");
3232
const location = expoLogIndex > 0 ? stack[expoLogIndex + 1] : stack[1];
33-
const lineOffset = global.__EXPO_ENV_PRELUDE_LINES__ || 0;
34-
args.push(location.file, location.lineNumber - lineOffset, location.column);
33+
args.push(location.file, location.lineNumber, location.column);
3534
return consoleFunc.apply(console, args);
3635
};
3736
}

packages/vscode-extension/src/builders/buildAndroid.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ export async function buildAndroid(
143143
),
144144
];
145145
// configureReactNativeOverrides init script is only necessary for RN versions older then 0.74.0 see comments in configureReactNativeOverrides.gradle for more details
146-
if (semver.lt(await getReactNativeVersion(), "0.74.0")) {
146+
if (semver.lt(getReactNativeVersion(), "0.74.0")) {
147147
gradleArgs.push(
148148
"--init-script", // configureReactNativeOverrides init script is used to patch React Android project, see comments in configureReactNativeOverrides.gradle for more details
149149
path.join(

packages/vscode-extension/src/debugging/DebugAdapter.ts

+33-7
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import {
1414
Source,
1515
StackFrame,
1616
} from "@vscode/debugadapter";
17+
import { getReactNativeVersion } from "../utilities/reactNative";
18+
import semver from "semver";
1719
import { DebugProtocol } from "@vscode/debugprotocol";
1820
import WebSocket from "ws";
1921
import { NullablePosition, SourceMapConsumer } from "source-map";
@@ -83,8 +85,8 @@ export class DebugAdapter extends DebugSession {
8385
private absoluteProjectPath: string;
8486
private projectPathAlias?: string;
8587
private threads: Array<Thread> = [];
86-
private sourceMaps: Array<[string, string, SourceMapConsumer]> = [];
87-
88+
private sourceMaps: Array<[string, string, SourceMapConsumer, number]> = [];
89+
private expoPreludeLineCount: number;
8890
private linesStartAt1 = true;
8991
private columnsStartAt1 = true;
9092

@@ -96,6 +98,7 @@ export class DebugAdapter extends DebugSession {
9698
this.absoluteProjectPath = configuration.absoluteProjectPath;
9799
this.projectPathAlias = configuration.projectPathAlias;
98100
this.connection = new WebSocket(configuration.websocketAddress);
101+
this.expoPreludeLineCount = configuration.expoPreludeLineCount;
99102

100103
this.connection.on("open", () => {
101104
// the below catch handler is used to ignore errors coming from non critical CDP messages we
@@ -150,7 +153,30 @@ export class DebugAdapter extends DebugSession {
150153
const decodedData = Buffer.from(base64Data, "base64").toString("utf-8");
151154
const sourceMap = JSON.parse(decodedData);
152155
const consumer = await new SourceMapConsumer(sourceMap);
153-
this.sourceMaps.push([message.params.url, message.params.scriptId, consumer]);
156+
157+
// We detect when a source map for the entire bundle is loaded by checking if __prelude__ module is present in the sources.
158+
const isMainBundle = sourceMap.sources.includes("__prelude__");
159+
160+
// Expo env plugin has a bug that causes the bundle to include so-called expo prelude module named __env__
161+
// which is not present in the source map. As a result, the line numbers are shifted by the amount of lines
162+
// the __env__ module adds. If we detect that main bundle is loaded, but __env__ is not there, we use the provided
163+
// expoPreludeLineCount which reflects the number of lines in __env__ module to offset the line numbers in the source map.
164+
const bundleContainsExpoPrelude = sourceMap.sources.includes("__env__");
165+
let lineOffset = 0;
166+
if (isMainBundle && !bundleContainsExpoPrelude && this.expoPreludeLineCount > 0) {
167+
Logger.debug(
168+
"Expo prelude lines were detected and an offset was set to:",
169+
this.expoPreludeLineCount
170+
);
171+
lineOffset = this.expoPreludeLineCount;
172+
}
173+
174+
this.sourceMaps.push([
175+
message.params.url,
176+
message.params.scriptId,
177+
consumer,
178+
lineOffset,
179+
]);
154180
this.updateBreakpointsInSource(message.params.url, consumer);
155181
}
156182

@@ -264,7 +290,7 @@ export class DebugAdapter extends DebugSession {
264290
let sourceLine1Based = lineNumber1Based;
265291
let sourceColumn0Based = columnNumber0Based;
266292

267-
this.sourceMaps.forEach(([url, id, consumer]) => {
293+
this.sourceMaps.forEach(([url, id, consumer, lineOffset]) => {
268294
// when we identify script by its URL we need to deal with a situation when the URL is sent with a different
269295
// hostname and port than the one we have registered in the source maps. The reason for that is that the request
270296
// that populates the source map (scriptParsed) is sent by metro, while the requests from breakpoints or logs
@@ -273,7 +299,7 @@ export class DebugAdapter extends DebugSession {
273299
if (id === scriptIdOrURL || compareIgnoringHost(url, scriptIdOrURL)) {
274300
scriptURL = url;
275301
const pos = consumer.originalPositionFor({
276-
line: lineNumber1Based,
302+
line: lineNumber1Based - lineOffset,
277303
column: columnNumber0Based,
278304
});
279305
if (pos.source != null) {
@@ -440,7 +466,7 @@ export class DebugAdapter extends DebugSession {
440466
}
441467
let position: NullablePosition = { line: null, column: null, lastColumn: null };
442468
let originalSourceURL: string = "";
443-
this.sourceMaps.forEach(([sourceURL, scriptId, consumer]) => {
469+
this.sourceMaps.forEach(([sourceURL, scriptId, consumer, lineOffset]) => {
444470
const sources = [];
445471
consumer.eachMapping((mapping) => {
446472
sources.push(mapping.source);
@@ -453,7 +479,7 @@ export class DebugAdapter extends DebugSession {
453479
});
454480
if (pos.line != null) {
455481
originalSourceURL = sourceURL;
456-
position = pos;
482+
position = { ...pos, line: pos.line + lineOffset };
457483
}
458484
});
459485
if (position.line === null) {

packages/vscode-extension/src/debugging/DebugSession.ts

+1
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ export class DebugSession implements Disposable {
5656
websocketAddress: websocketAddress,
5757
absoluteProjectPath: getAppRootFolder(),
5858
projectPathAlias: this.metro.isUsingNewDebugger ? "/[metro-project]" : undefined,
59+
expoPreludeLineCount: this.metro.expoPreludeLineCount,
5960
},
6061
{
6162
suppressDebugStatusbar: true,

packages/vscode-extension/src/project/metro.ts

+10
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ type MetroEvent =
4646
transformedFileCount: number;
4747
totalFileCount: number;
4848
}
49+
| { type: "RNIDE_expo_env_prelude_lines"; lineCount: number }
4950
| {
5051
type: "RNIDE_initialize_done";
5152
port: number;
@@ -66,6 +67,7 @@ export class Metro implements Disposable {
6667
private _port = 0;
6768
private startPromise: Promise<void> | undefined;
6869
private usesNewDebugger?: Boolean;
70+
private _expoPreludeLineCount = 0;
6971

7072
constructor(private readonly devtools: Devtools, private readonly delegate: MetroDelegate) {}
7173

@@ -80,6 +82,10 @@ export class Metro implements Disposable {
8082
return this._port;
8183
}
8284

85+
public get expoPreludeLineCount() {
86+
return this._expoPreludeLineCount;
87+
}
88+
8389
public dispose() {
8490
this.subprocess?.kill(9);
8591
}
@@ -219,6 +225,10 @@ export class Metro implements Disposable {
219225
}
220226

221227
switch (event.type) {
228+
case "RNIDE_expo_env_prelude_lines":
229+
this._expoPreludeLineCount = event.lineCount;
230+
Logger.debug("Expo prelude line offset was set to: ", this._expoPreludeLineCount);
231+
break;
222232
case "RNIDE_initialize_done":
223233
this._port = event.port;
224234
Logger.info(`Metro started on port ${this._port}`);

packages/vscode-extension/src/utilities/reactNative.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import path from "path";
22
import { getAppRootFolder } from "./extensionContext";
33

4-
export async function getReactNativeVersion() {
4+
export function getReactNativeVersion() {
55
const workspacePath = getAppRootFolder();
66
const reactNativeRoot = path.dirname(require.resolve("react-native", { paths: [workspacePath] }));
77
const packageJsonPath = path.join(reactNativeRoot, "package.json");

0 commit comments

Comments
 (0)