Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix breakpoints in RN 76 apps #644

Merged
merged 3 commits into from
Oct 22, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions packages/vscode-extension/lib/metro_helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,14 @@ function adaptMetroConfig(config) {
const ReporterImpl = require("./metro_reporter");
config.reporter = new ReporterImpl();

process.stdout.write(
JSON.stringify({
type: "RNIDE_watch_folders",
watchFolders: [config.projectRoot, ...config.watchFolders], // metro internally adds projectRoot as first entry to watch folders
})
);
process.stdout.write("\n");

return config;
}

Expand Down
70 changes: 39 additions & 31 deletions packages/vscode-extension/src/debugging/DebugAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,7 @@
export class DebugAdapter extends DebugSession {
private variableStore: VariableStore = new VariableStore();
private connection: WebSocket;
private absoluteProjectPath: string;
private projectPathAlias?: string;
private sourceMapAliases?: Array<[string, string]>;
private threads: Array<Thread> = [];
private sourceMaps: Array<[string, string, SourceMapConsumer]> = [];

Expand All @@ -93,8 +92,7 @@

constructor(configuration: DebugConfiguration) {
super();
this.absoluteProjectPath = configuration.absoluteProjectPath;
this.projectPathAlias = configuration.projectPathAlias;
this.sourceMapAliases = configuration.sourceMapAliases;
this.connection = new WebSocket(configuration.websocketAddress);

this.connection.on("open", () => {
Expand All @@ -107,7 +105,7 @@
this.sendCDPMessage("Debugger.setPauseOnExceptions", { state: "none" });
this.sendCDPMessage("Debugger.setAsyncCallStackDepth", { maxDepth: 32 }).catch(ignoreError);
this.sendCDPMessage("Debugger.setBlackboxPatterns", { patterns: [] }).catch(ignoreError);
this.sendCDPMessage("Runtime.runIfWaitingForDebugger", {});
this.sendCDPMessage("Runtime.runIfWaitingForDebugger", {}).catch(ignoreError);
this.sendCDPMessage("Runtime.evaluate", {
expression: "__RNIDE_onDebuggerConnected()",
});
Expand Down Expand Up @@ -254,6 +252,30 @@
]);
}

private toAbsoluteFilePath(sourceMapPath: string) {
if (this.sourceMapAliases) {
for (const [alias, absoluteFilePath] of this.sourceMapAliases) {
if (sourceMapPath.startsWith(alias)) {
// URL may contain ".." fragments, so we want to resolve it to a proper absolute file path
return path.resolve(path.join(absoluteFilePath, sourceMapPath.slice(alias.length)));
}
}
}
return sourceMapPath;
}

private toSourceMapFilePath(sourceAbsoluteFilePath: string) {
if (this.sourceMapAliases) {
// we return the first alias from the list
for (const [alias, absoluteFilePath] of this.sourceMapAliases) {
if (absoluteFilePath.startsWith(absoluteFilePath)) {
return path.join(alias, path.relative(absoluteFilePath, sourceAbsoluteFilePath));
}
}
}
return sourceAbsoluteFilePath;
}

private findOriginalPosition(
scriptIdOrURL: string,
lineNumber1Based: number,
Expand All @@ -276,24 +298,20 @@
line: lineNumber1Based,
column: columnNumber0Based,
});
if (pos.source != null) {

Check warning on line 301 in packages/vscode-extension/src/debugging/DebugAdapter.ts

View workflow job for this annotation

GitHub Actions / check

Expected '!==' and instead saw '!='
sourceURL = pos.source;
}
if (pos.line != null) {

Check warning on line 304 in packages/vscode-extension/src/debugging/DebugAdapter.ts

View workflow job for this annotation

GitHub Actions / check

Expected '!==' and instead saw '!='
sourceLine1Based = pos.line;
}
if (pos.column != null) {

Check warning on line 307 in packages/vscode-extension/src/debugging/DebugAdapter.ts

View workflow job for this annotation

GitHub Actions / check

Expected '!==' and instead saw '!='
sourceColumn0Based = pos.column;
}
}
});
if (this.projectPathAlias) {
// URL may contain ".." fragments, so we want to resolve it to a proper absolute file path
sourceURL = path.resolve(sourceURL.replace(this.projectPathAlias, this.absoluteProjectPath));
}

return {
sourceURL,
sourceURL: this.toAbsoluteFilePath(sourceURL),
lineNumber1Based: sourceLine1Based,
columnNumber0Based: sourceColumn0Based,
scriptURL,
Expand Down Expand Up @@ -431,27 +449,17 @@
}

private toGeneratedPosition(file: string, lineNumber1Based: number, columnNumber0Based: number) {
let genFileName = file;
if (this.projectPathAlias) {
// we first convert the file path to be relative to project:
const fileRelative = path.relative(this.absoluteProjectPath, file);
// no we append the project path alias that represents the root of the project
genFileName = path.join(this.projectPathAlias, fileRelative);
}
let sourceMapFilePath = this.toSourceMapFilePath(file);
let position: NullablePosition = { line: null, column: null, lastColumn: null };
let originalSourceURL: string = "";
this.sourceMaps.forEach(([sourceURL, scriptId, consumer]) => {
const sources = [];
consumer.eachMapping((mapping) => {
sources.push(mapping.source);
});
const pos = consumer.generatedPositionFor({
source: genFileName,
source: sourceMapFilePath,
line: lineNumber1Based,
column: columnNumber0Based,
bias: SourceMapConsumer.LEAST_UPPER_BOUND,
});
if (pos.line != null) {

Check warning on line 462 in packages/vscode-extension/src/debugging/DebugAdapter.ts

View workflow job for this annotation

GitHub Actions / check

Expected '!==' and instead saw '!='
originalSourceURL = sourceURL;
position = pos;
}
Expand Down Expand Up @@ -489,21 +497,21 @@
// this method gets called after we are informed that a new script has been parsed. If we
// had breakpoints set in that script, we need to let the runtime know about it

const pathsToUpdate = new Set<string>();
consumer.eachMapping((mapping) => {
if (this.breakpoints.has(mapping.source)) {
pathsToUpdate.add(mapping.source);
}
});
// the number of consumer mapping entries can be close to the number of symbols in the source file.
// we optimize the process by collecting unique source URLs which map to actual individual source files.
// note: apparently despite the TS types from the source-map library, mapping.source can be null
const uniqueSourceMapPaths = new Set<string>();
consumer.eachMapping((mapping) => mapping.source && uniqueSourceMapPaths.add(mapping.source));

pathsToUpdate.forEach((pathToUpdate) => {
const breakpoints = this.breakpoints.get(pathToUpdate) || [];
uniqueSourceMapPaths.forEach((sourceMapPath) => {
const absoluteFilePath = this.toAbsoluteFilePath(sourceMapPath);
const breakpoints = this.breakpoints.get(absoluteFilePath) || [];
breakpoints.forEach(async (bp) => {
if (bp.verified) {
this.sendCDPMessage("Debugger.removeBreakpoint", { breakpointId: bp.getId() });
}
const newId = await this.setCDPBreakpoint(
pathToUpdate,
sourceMapPath,
this.linesStartAt1 ? bp.line : bp.line + 1,
this.columnsStartAt1 ? (bp.column || 1) - 1 : bp.column || 0
);
Expand Down
12 changes: 10 additions & 2 deletions packages/vscode-extension/src/debugging/DebugSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,15 +47,23 @@ export class DebugSession implements Disposable {
return false;
}

let sourceMapAliases: Array<[string, string]> = [];
if (this.metro.isUsingNewDebugger && this.metro.watchFolders.length > 0) {
// first entry in watchFolders is the project root
sourceMapAliases.push(["/[metro-project]/", this.metro.watchFolders[0]]);
this.metro.watchFolders.forEach((watchFolder, index) => {
sourceMapAliases.push([`/[metro-watchFolders]/${index}/`, watchFolder]);
});
}

const debugStarted = await debug.startDebugging(
undefined,
{
type: "com.swmansion.react-native-debugger",
name: "Radon IDE Debugger",
request: "attach",
websocketAddress: websocketAddress,
absoluteProjectPath: getAppRootFolder(),
projectPathAlias: this.metro.isUsingNewDebugger ? "/[metro-project]" : undefined,
sourceMapAliases,
},
{
suppressDebugStatusbar: true,
Expand Down
16 changes: 16 additions & 0 deletions packages/vscode-extension/src/project/metro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import { shouldUseExpoCLI } from "../utilities/expoCli";
import { Devtools } from "./devtools";
import { getLaunchConfiguration } from "../utilities/launchConfiguration";
import WebSocket from "ws";

Check warning on line 11 in packages/vscode-extension/src/project/metro.ts

View workflow job for this annotation

GitHub Actions / check

`ws` import should occur before import of `../utilities/subprocess`

export interface MetroDelegate {
onBundleError(): void;
Expand Down Expand Up @@ -50,6 +50,10 @@
type: "RNIDE_initialize_done";
port: number;
}
| {
type: "RNIDE_watch_folders";
watchFolders: string[];
}
| {
type: "client_log";
level: "error";
Expand All @@ -64,6 +68,7 @@
export class Metro implements Disposable {
private subprocess?: ChildProcess;
private _port = 0;
private _watchFolders: string[] | undefined = undefined;
private startPromise: Promise<void> | undefined;
private usesNewDebugger?: Boolean;

Expand All @@ -80,6 +85,13 @@
return this._port;
}

public get watchFolders() {
if (this._watchFolders === undefined) {
throw new Error("Attempting to read watchFolders before metro has started");
}
return this._watchFolders;
}

public dispose() {
this.subprocess?.kill(9);
}
Expand Down Expand Up @@ -224,6 +236,10 @@
Logger.info(`Metro started on port ${this._port}`);
resolve();
break;
case "RNIDE_watch_folders":
this._watchFolders = event.watchFolders;
Logger.info("Captured metro watch folders", this._watchFolders);
break;
case "bundle_build_failed":
this.delegate.onBundleError();
break;
Expand Down
Loading