Skip to content

Commit 54b4f46

Browse files
motiz88meta-codesync[bot]
authored andcommitted
Add shared C++ error parser and ANSI renderer (#56553)
Summary: Pull Request resolved: #56553 Add a platform-independent C++ library (`ReactCommon/react/debug/redbox/`) for error message parsing and ANSI escape sequence rendering, shared by both iOS and Android. `RedBoxErrorParser` — C++ port of `parseLogBoxException`. Classifies Metro errors, Babel transform errors, bundle loading errors, and code frame errors into a structured `ParsedError`. `AnsiParser` — converts ANSI SGR sequences into styled spans with foreground/background colors using the Afterglow theme. Uses `facebook::react::unstable_redbox` namespace to exclude from C++ API snapshots. Changelog: [Internal] Reviewed By: robhogan Differential Revision: D101357709 fbshipit-source-id: d2ecf9d12897e00f9590e1bec57ecf5d5895fcd5
1 parent f81c991 commit 54b4f46

9 files changed

Lines changed: 590 additions & 1 deletion

File tree

packages/react-native/Package.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ let reactRendererConsistency = RNTarget(
9393
let reactDebug = RNTarget(
9494
name: .reactDebug,
9595
path: "ReactCommon/react/debug",
96+
excludedPaths: ["tests", "redbox/tests"],
9697
dependencies: [.reactNativeDependencies]
9798
)
9899
/// React-jsi.podspec

packages/react-native/ReactCommon/react/debug/CMakeLists.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ set(CMAKE_VERBOSE_MAKEFILE on)
99
include(${REACT_COMMON_DIR}/cmake-utils/react-native-flags.cmake)
1010

1111
file(GLOB react_debug_SRC CONFIGURE_DEPENDS *.cpp)
12-
add_library(react_debug OBJECT ${react_debug_SRC})
12+
file(GLOB react_debug_redbox_SRC CONFIGURE_DEPENDS redbox/*.cpp)
13+
add_library(react_debug OBJECT ${react_debug_SRC} ${react_debug_redbox_SRC})
1314

1415
target_include_directories(react_debug PUBLIC ${REACT_COMMON_DIR})
1516

packages/react-native/ReactCommon/react/debug/React-debug.podspec

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ Pod::Spec.new do |s|
2626
s.platforms = min_supported_versions
2727
s.source = source
2828
s.source_files = podspec_sources("**/*.{cpp,h}", "**/*.h")
29+
s.exclude_files = "**/tests/**/*.{cpp,h}"
2930
s.header_dir = "react/debug"
3031
s.pod_target_xcconfig = { "CLANG_CXX_LANGUAGE_STANDARD" => rct_cxx_language_standard(),
3132
"DEFINES_MODULE" => "YES" }
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
#include "AnsiParser.h"
9+
10+
#include <optional>
11+
12+
#include <regex> // NOLINT(facebook-hte-BadInclude-regex)
13+
14+
// @lint-ignore-every CLANGTIDY facebook-hte-StdRegexIsAwful
15+
namespace facebook::react::unstable_redbox {
16+
17+
namespace {
18+
19+
// Afterglow theme colors (matching AnsiHighlight.js)
20+
std::optional<AnsiColor> ansiColor(int code) {
21+
switch (code) {
22+
case 30:
23+
return AnsiColor{.r = 27, .g = 27, .b = 27}; // black
24+
case 31:
25+
return AnsiColor{.r = 187, .g = 86, .b = 83}; // red
26+
case 32:
27+
return AnsiColor{.r = 144, .g = 157, .b = 98}; // green
28+
case 33:
29+
return AnsiColor{.r = 234, .g = 193, .b = 121}; // yellow
30+
case 34:
31+
return AnsiColor{.r = 125, .g = 169, .b = 199}; // blue
32+
case 35:
33+
return AnsiColor{.r = 176, .g = 101, .b = 151}; // magenta
34+
case 36:
35+
return AnsiColor{.r = 140, .g = 220, .b = 216}; // cyan
36+
case 37:
37+
return std::nullopt; // white = default
38+
case 90:
39+
return AnsiColor{.r = 98, .g = 98, .b = 98}; // bright black
40+
case 91:
41+
return AnsiColor{.r = 187, .g = 86, .b = 83}; // bright red
42+
case 92:
43+
return AnsiColor{.r = 144, .g = 157, .b = 98}; // bright green
44+
case 93:
45+
return AnsiColor{.r = 234, .g = 193, .b = 121}; // bright yellow
46+
case 94:
47+
return AnsiColor{.r = 125, .g = 169, .b = 199}; // bright blue
48+
case 95:
49+
return AnsiColor{.r = 176, .g = 101, .b = 151}; // bright magenta
50+
case 96:
51+
return AnsiColor{.r = 140, .g = 220, .b = 216}; // bright cyan
52+
case 97:
53+
return AnsiColor{.r = 247, .g = 247, .b = 247}; // bright white
54+
default:
55+
return std::nullopt;
56+
}
57+
}
58+
59+
const std::regex& ansiRegex() {
60+
static const std::regex re(R"(\x1b\[([0-9;]*)m)");
61+
return re;
62+
}
63+
64+
int parseSgrCode(const std::string& params, size_t& pos) {
65+
size_t next = params.find(';', pos);
66+
if (next == std::string::npos) {
67+
next = params.size();
68+
}
69+
int code = 0;
70+
for (size_t i = pos; i < next; ++i) {
71+
code = code * 10 + (params[i] - '0');
72+
}
73+
pos = next + 1;
74+
return code;
75+
}
76+
77+
} // namespace
78+
79+
std::vector<AnsiSpan> parseAnsi(const std::string& text) {
80+
std::vector<AnsiSpan> spans;
81+
std::optional<AnsiColor> currentFg;
82+
std::optional<AnsiColor> currentBg;
83+
auto it = std::sregex_iterator(text.begin(), text.end(), ansiRegex());
84+
auto end = std::sregex_iterator();
85+
size_t lastEnd = 0;
86+
87+
for (; it != end; ++it) {
88+
const auto& match = *it;
89+
auto matchStart = static_cast<size_t>(match.position());
90+
91+
if (matchStart > lastEnd) {
92+
spans.push_back(
93+
AnsiSpan{
94+
.text = text.substr(lastEnd, matchStart - lastEnd),
95+
.foregroundColor = currentFg,
96+
.backgroundColor = currentBg});
97+
}
98+
lastEnd = matchStart + match.length();
99+
100+
std::string params = match[1].str();
101+
// ESC[m (no params) is equivalent to ESC[0m (reset all attributes)
102+
if (params.empty()) {
103+
currentFg = std::nullopt;
104+
currentBg = std::nullopt;
105+
}
106+
size_t pos = 0;
107+
while (pos < params.size()) {
108+
int code = parseSgrCode(params, pos);
109+
if (code == 0) {
110+
currentFg = std::nullopt;
111+
currentBg = std::nullopt;
112+
} else if ((code >= 30 && code <= 37) || (code >= 90 && code <= 97)) {
113+
currentFg = ansiColor(code);
114+
} else if ((code >= 40 && code <= 47) || (code >= 100 && code <= 107)) {
115+
currentBg = ansiColor(code - 10);
116+
} else if (code == 39) {
117+
currentFg = std::nullopt;
118+
} else if (code == 49) {
119+
currentBg = std::nullopt;
120+
}
121+
}
122+
}
123+
124+
if (lastEnd < text.size()) {
125+
spans.push_back(
126+
AnsiSpan{
127+
.text = text.substr(lastEnd),
128+
.foregroundColor = currentFg,
129+
.backgroundColor = currentBg});
130+
}
131+
132+
return spans;
133+
}
134+
135+
std::string stripAnsi(const std::string& text) {
136+
return std::regex_replace(text, ansiRegex(), "");
137+
}
138+
139+
} // namespace facebook::react::unstable_redbox
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
#pragma once
9+
10+
#include <optional>
11+
#include <string>
12+
#include <vector>
13+
14+
namespace facebook::react::unstable_redbox {
15+
16+
struct AnsiColor {
17+
uint8_t r, g, b;
18+
};
19+
20+
struct AnsiSpan {
21+
std::string text;
22+
std::optional<AnsiColor> foregroundColor;
23+
std::optional<AnsiColor> backgroundColor;
24+
};
25+
26+
/**
27+
* Parse ANSI escape sequences in text and produce a list of styled spans.
28+
* Uses the Afterglow color theme (matching LogBox's AnsiHighlight.js).
29+
*/
30+
std::vector<AnsiSpan> parseAnsi(const std::string &text);
31+
32+
/** Strip all ANSI escape sequences from text. */
33+
std::string stripAnsi(const std::string &text);
34+
35+
} // namespace facebook::react::unstable_redbox
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
#include "RedBoxErrorParser.h"
9+
10+
#include <regex> // NOLINT(facebook-hte-BadInclude-regex)
11+
#include <unordered_set>
12+
13+
// @lint-ignore-every CLANGTIDY facebook-hte-StdRegexIsAwful
14+
namespace facebook::react::unstable_redbox {
15+
16+
namespace {
17+
18+
const std::regex& metroErrorRegex() {
19+
static const std::regex re(
20+
R"(^(?:InternalError Metro has encountered an error:) (.*): (.*) \((\d+):(\d+)\)\n\n([\s\S]+))");
21+
return re;
22+
}
23+
24+
const std::regex& babelTransformErrorRegex() {
25+
static const std::regex re(
26+
R"(^(?:TransformError )?(?:SyntaxError: |ReferenceError: )(.*): (.*) \((\d+):(\d+)\)\n\n([\s\S]+))");
27+
return re;
28+
}
29+
30+
const std::regex& bundleLoadErrorRegex() {
31+
static const std::regex re(R"(^(\w+) in (\S+): (.+) \((\d+):(\d+)\))");
32+
return re;
33+
}
34+
35+
const std::regex& babelCodeFrameErrorRegex() {
36+
static const std::regex re(
37+
R"(^(?:TransformError )?(?:.*):? (?:.*?)(\/.*): ([\s\S]+?)\n([ >]{2}[\d\s]+ \|[\s\S]+|\x1b[\s\S]+))");
38+
return re;
39+
}
40+
41+
bool startsWithTransformError(const std::string& msg) {
42+
return msg.rfind("TransformError ", 0) == 0;
43+
}
44+
45+
const std::unordered_set<std::string>& knownBundleLoadErrorTypes() {
46+
static const std::unordered_set<std::string> types{
47+
"SyntaxError", "ReferenceError", "TypeError", "UnableToResolveError"};
48+
return types;
49+
}
50+
51+
} // namespace
52+
53+
ParsedError parseErrorMessage(
54+
const std::string& message,
55+
const std::string& name,
56+
const std::string& componentStack,
57+
bool isFatal) {
58+
std::smatch match;
59+
60+
if (message.empty()) {
61+
return ParsedError{
62+
.title = isFatal ? "Uncaught Error" : "Error",
63+
.message = "",
64+
.codeFrame = std::nullopt,
65+
.isCompileError = false,
66+
};
67+
}
68+
69+
// 1. Metro internal error
70+
if (std::regex_search(message, match, metroErrorRegex())) {
71+
return ParsedError{
72+
.title = match[1].str().empty() ? "Metro Error" : match[1].str(),
73+
.message = match[2].str(),
74+
.codeFrame =
75+
CodeFrame{
76+
.content = match[5].str(),
77+
.fileName = "",
78+
.row = std::stoi(match[3].str()),
79+
.column = std::stoi(match[4].str()),
80+
},
81+
.isCompileError = true,
82+
};
83+
}
84+
85+
// 2. Babel transform error
86+
if (std::regex_search(message, match, babelTransformErrorRegex())) {
87+
return ParsedError{
88+
.title = "Syntax Error",
89+
.message = match[2].str(),
90+
.codeFrame =
91+
CodeFrame{
92+
.content = match[5].str(),
93+
.fileName = match[1].str(),
94+
.row = std::stoi(match[3].str()),
95+
.column = std::stoi(match[4].str()),
96+
},
97+
.isCompileError = true,
98+
};
99+
}
100+
101+
// 3. Bundle loading error: "ErrorType in /path: message (line:col)"
102+
if (std::regex_search(message, match, bundleLoadErrorRegex())) {
103+
const auto& errorType = match[1].str();
104+
if (knownBundleLoadErrorTypes().count(errorType) > 0) {
105+
std::string title = errorType == "UnableToResolveError"
106+
? "Module Not Found"
107+
: "Syntax Error";
108+
std::optional<std::string> codeFrameContent;
109+
auto newlinePos = message.find('\n');
110+
if (newlinePos != std::string::npos) {
111+
codeFrameContent = message.substr(newlinePos + 1);
112+
}
113+
return ParsedError{
114+
.title = title,
115+
.message = match[3].str(),
116+
.codeFrame =
117+
CodeFrame{
118+
.content = codeFrameContent.value_or(""),
119+
.fileName = match[2].str(),
120+
.row = std::stoi(match[4].str()),
121+
.column = std::stoi(match[5].str()),
122+
},
123+
.isCompileError = true,
124+
};
125+
}
126+
}
127+
128+
// 4. Babel code frame error
129+
if (std::regex_search(message, match, babelCodeFrameErrorRegex())) {
130+
return ParsedError{
131+
.title = "Syntax Error",
132+
.message = match[2].str(),
133+
.codeFrame =
134+
CodeFrame{
135+
.content = match[3].str(),
136+
.fileName = match[1].str(),
137+
},
138+
.isCompileError = true,
139+
};
140+
}
141+
142+
// 5. Generic transform error (no code frame)
143+
if (startsWithTransformError(message)) {
144+
return ParsedError{
145+
.title = "Syntax Error",
146+
.message = message,
147+
.codeFrame = std::nullopt,
148+
.isCompileError = true,
149+
};
150+
}
151+
152+
// 6. Determine title from context (matching LogBoxInspectorHeader title map)
153+
std::string title;
154+
if (!name.empty()) {
155+
title = name;
156+
} else if (!componentStack.empty()) {
157+
title = "Render Error";
158+
} else if (isFatal) {
159+
title = "Uncaught Error";
160+
} else {
161+
title = "Error";
162+
}
163+
return ParsedError{
164+
.title = title,
165+
.message = message,
166+
.codeFrame = std::nullopt,
167+
.isCompileError = false,
168+
};
169+
}
170+
171+
} // namespace facebook::react::unstable_redbox

0 commit comments

Comments
 (0)