diff --git a/packages/vscode-extension/lib/metro_helpers.js b/packages/vscode-extension/lib/metro_helpers.js index ca3ef42d6..9dcc19c23 100644 --- a/packages/vscode-extension/lib/metro_helpers.js +++ b/packages/vscode-extension/lib/metro_helpers.js @@ -42,15 +42,19 @@ function adaptMetroConfig(config) { } } else if (module.path === "__env__") { // this handles @expo/env plugin, which is used to inject environment variables - // the code below instantiates a global variable __EXPO_ENV_PRELUDE_LINES__ that stores - // the number of lines in the prelude. This is used to calculate the line number offset - // when reporting line numbers from the JS runtime. The reason why this is needed, is that + // the code below exposes the number of lines in the prelude. + // This is used to calculate the line number offset + // when reporting line numbers from the JS runtime, breakpoints + // and uncaught exceptions. The reason why this is needed, is that // metro doesn't include __env__ prelude in the source map resulting in the source map - // transformation getting shifted by the number of lines in the prelude. - const expoEnvCode = module.output[0].data.code; - if (!expoEnvCode.includes("__EXPO_ENV_PRELUDE_LINES__")) { - module.output[0].data.code = `${expoEnvCode};var __EXPO_ENV_PRELUDE_LINES__=${module.output[0].data.lineCount};`; - } + // transformation getting shifted by the number of lines in the expo generated prelude. + process.stdout.write( + JSON.stringify({ + type: "RNIDE_expo_env_prelude_lines", + lineCount: module.output[0].data.lineCount, + }) + ); + process.stdout.write("\n"); } return origProcessModuleFilter(module); }; diff --git a/packages/vscode-extension/lib/runtime.js b/packages/vscode-extension/lib/runtime.js index cfee23dd5..b2f410df4 100644 --- a/packages/vscode-extension/lib/runtime.js +++ b/packages/vscode-extension/lib/runtime.js @@ -25,8 +25,7 @@ function wrapConsole(consoleFunc) { const stack = parseErrorStack(new Error().stack); const expoLogIndex = stack.findIndex((frame) => frame.methodName === "__expoConsoleLog"); const location = expoLogIndex > 0 ? stack[expoLogIndex + 1] : stack[1]; - const lineOffset = global.__EXPO_ENV_PRELUDE_LINES__ || 0; - args.push(location.file, location.lineNumber - lineOffset, location.column); + args.push(location.file, location.lineNumber, location.column); return consoleFunc.apply(console, args); }; } diff --git a/packages/vscode-extension/src/builders/buildAndroid.ts b/packages/vscode-extension/src/builders/buildAndroid.ts index 4a0af62f2..6f8e49b86 100644 --- a/packages/vscode-extension/src/builders/buildAndroid.ts +++ b/packages/vscode-extension/src/builders/buildAndroid.ts @@ -143,7 +143,7 @@ export async function buildAndroid( ), ]; // configureReactNativeOverrides init script is only necessary for RN versions older then 0.74.0 see comments in configureReactNativeOverrides.gradle for more details - if (semver.lt(await getReactNativeVersion(), "0.74.0")) { + if (semver.lt(getReactNativeVersion(), "0.74.0")) { gradleArgs.push( "--init-script", // configureReactNativeOverrides init script is used to patch React Android project, see comments in configureReactNativeOverrides.gradle for more details path.join( diff --git a/packages/vscode-extension/src/debugging/DebugAdapter.ts b/packages/vscode-extension/src/debugging/DebugAdapter.ts index abbc2838b..50e786a90 100644 --- a/packages/vscode-extension/src/debugging/DebugAdapter.ts +++ b/packages/vscode-extension/src/debugging/DebugAdapter.ts @@ -14,6 +14,8 @@ import { Source, StackFrame, } from "@vscode/debugadapter"; +import { getReactNativeVersion } from "../utilities/reactNative"; +import semver from "semver"; import { DebugProtocol } from "@vscode/debugprotocol"; import WebSocket from "ws"; import { NullablePosition, SourceMapConsumer } from "source-map"; @@ -83,8 +85,8 @@ export class DebugAdapter extends DebugSession { private absoluteProjectPath: string; private projectPathAlias?: string; private threads: Array = []; - private sourceMaps: Array<[string, string, SourceMapConsumer]> = []; - + private sourceMaps: Array<[string, string, SourceMapConsumer, number]> = []; + private expoPreludeLineCount: number; private linesStartAt1 = true; private columnsStartAt1 = true; @@ -96,6 +98,7 @@ export class DebugAdapter extends DebugSession { this.absoluteProjectPath = configuration.absoluteProjectPath; this.projectPathAlias = configuration.projectPathAlias; this.connection = new WebSocket(configuration.websocketAddress); + this.expoPreludeLineCount = configuration.expoPreludeLineCount; this.connection.on("open", () => { // 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 { const decodedData = Buffer.from(base64Data, "base64").toString("utf-8"); const sourceMap = JSON.parse(decodedData); const consumer = await new SourceMapConsumer(sourceMap); - this.sourceMaps.push([message.params.url, message.params.scriptId, consumer]); + + // We detect when a source map for the entire bundle is loaded by checking if __prelude__ module is present in the sources. + const isMainBundle = sourceMap.sources.includes("__prelude__"); + + // Expo env plugin has a bug that causes the bundle to include so-called expo prelude module named __env__ + // which is not present in the source map. As a result, the line numbers are shifted by the amount of lines + // the __env__ module adds. If we detect that main bundle is loaded, but __env__ is not there, we use the provided + // expoPreludeLineCount which reflects the number of lines in __env__ module to offset the line numbers in the source map. + const bundleContainsExpoPrelude = sourceMap.sources.includes("__env__"); + let lineOffset = 0; + if (isMainBundle && !bundleContainsExpoPrelude && this.expoPreludeLineCount > 0) { + Logger.debug( + "Expo prelude lines were detected and an offset was set to:", + this.expoPreludeLineCount + ); + lineOffset = this.expoPreludeLineCount; + } + + this.sourceMaps.push([ + message.params.url, + message.params.scriptId, + consumer, + lineOffset, + ]); this.updateBreakpointsInSource(message.params.url, consumer); } @@ -264,7 +290,7 @@ export class DebugAdapter extends DebugSession { let sourceLine1Based = lineNumber1Based; let sourceColumn0Based = columnNumber0Based; - this.sourceMaps.forEach(([url, id, consumer]) => { + this.sourceMaps.forEach(([url, id, consumer, lineOffset]) => { // when we identify script by its URL we need to deal with a situation when the URL is sent with a different // hostname and port than the one we have registered in the source maps. The reason for that is that the request // 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 { if (id === scriptIdOrURL || compareIgnoringHost(url, scriptIdOrURL)) { scriptURL = url; const pos = consumer.originalPositionFor({ - line: lineNumber1Based, + line: lineNumber1Based - lineOffset, column: columnNumber0Based, }); if (pos.source != null) { @@ -440,7 +466,7 @@ export class DebugAdapter extends DebugSession { } let position: NullablePosition = { line: null, column: null, lastColumn: null }; let originalSourceURL: string = ""; - this.sourceMaps.forEach(([sourceURL, scriptId, consumer]) => { + this.sourceMaps.forEach(([sourceURL, scriptId, consumer, lineOffset]) => { const sources = []; consumer.eachMapping((mapping) => { sources.push(mapping.source); @@ -453,7 +479,7 @@ export class DebugAdapter extends DebugSession { }); if (pos.line != null) { originalSourceURL = sourceURL; - position = pos; + position = { ...pos, line: pos.line + lineOffset }; } }); if (position.line === null) { diff --git a/packages/vscode-extension/src/debugging/DebugSession.ts b/packages/vscode-extension/src/debugging/DebugSession.ts index 93a03d531..fa473003f 100644 --- a/packages/vscode-extension/src/debugging/DebugSession.ts +++ b/packages/vscode-extension/src/debugging/DebugSession.ts @@ -56,6 +56,7 @@ export class DebugSession implements Disposable { websocketAddress: websocketAddress, absoluteProjectPath: getAppRootFolder(), projectPathAlias: this.metro.isUsingNewDebugger ? "/[metro-project]" : undefined, + expoPreludeLineCount: this.metro.expoPreludeLineCount, }, { suppressDebugStatusbar: true, diff --git a/packages/vscode-extension/src/project/metro.ts b/packages/vscode-extension/src/project/metro.ts index 109af4c8b..9cc49e508 100644 --- a/packages/vscode-extension/src/project/metro.ts +++ b/packages/vscode-extension/src/project/metro.ts @@ -46,6 +46,7 @@ type MetroEvent = transformedFileCount: number; totalFileCount: number; } + | { type: "RNIDE_expo_env_prelude_lines"; lineCount: number } | { type: "RNIDE_initialize_done"; port: number; @@ -66,6 +67,7 @@ export class Metro implements Disposable { private _port = 0; private startPromise: Promise | undefined; private usesNewDebugger?: Boolean; + private _expoPreludeLineCount = 0; constructor(private readonly devtools: Devtools, private readonly delegate: MetroDelegate) {} @@ -80,6 +82,10 @@ export class Metro implements Disposable { return this._port; } + public get expoPreludeLineCount() { + return this._expoPreludeLineCount; + } + public dispose() { this.subprocess?.kill(9); } @@ -207,6 +213,10 @@ export class Metro implements Disposable { } switch (event.type) { + case "RNIDE_expo_env_prelude_lines": + this._expoPreludeLineCount = event.lineCount; + Logger.debug("Expo prelude line offset was set to: ", this._expoPreludeLineCount); + break; case "RNIDE_initialize_done": this._port = event.port; Logger.info(`Metro started on port ${this._port}`); diff --git a/packages/vscode-extension/src/utilities/reactNative.ts b/packages/vscode-extension/src/utilities/reactNative.ts index 5e4e75d80..c2c64cf80 100644 --- a/packages/vscode-extension/src/utilities/reactNative.ts +++ b/packages/vscode-extension/src/utilities/reactNative.ts @@ -1,7 +1,7 @@ import path from "path"; import { getAppRootFolder } from "./extensionContext"; -export async function getReactNativeVersion() { +export function getReactNativeVersion() { const workspacePath = getAppRootFolder(); const reactNativeRoot = path.dirname(require.resolve("react-native", { paths: [workspacePath] })); const packageJsonPath = path.join(reactNativeRoot, "package.json");