@ceraph/react-native-mcp 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +196 -0
- package/dist/cli.d.ts +9 -0
- package/dist/cli.js +16 -0
- package/dist/error-parser.d.ts +71 -0
- package/dist/error-parser.js +345 -0
- package/dist/expo-manager.d.ts +134 -0
- package/dist/expo-manager.js +561 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +442 -0
- package/dist/init.d.ts +8 -0
- package/dist/init.js +235 -0
- package/dist/prebuild-detector.d.ts +49 -0
- package/dist/prebuild-detector.js +215 -0
- package/dist/screen.d.ts +95 -0
- package/dist/screen.js +357 -0
- package/package.json +42 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ejike Eze / IkeStudios
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# @ceraph/react-native-mcp
|
|
2
|
+
|
|
3
|
+
MCP server for React Native and Expo development. Automatic build error capture, console monitoring, reliable screen interactions, and prebuild detection.
|
|
4
|
+
|
|
5
|
+
Works with any MCP client: Claude Code, Cursor, Codex, Windsurf, and others.
|
|
6
|
+
|
|
7
|
+
## Requirements
|
|
8
|
+
|
|
9
|
+
- Node.js 18+
|
|
10
|
+
- macOS (for iOS Simulator/device support)
|
|
11
|
+
- [WebDriverAgent](https://github.com/appium/WebDriverAgent) running on `localhost:8100`
|
|
12
|
+
- [`mobile-mcp`](https://github.com/mobile-next/mobile-mcp) for screenshots, swipe, and device management
|
|
13
|
+
- Expo dev client or prebuilt app (Expo Go is not supported)
|
|
14
|
+
|
|
15
|
+
## Quick Setup
|
|
16
|
+
|
|
17
|
+
Run this from your project root:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npx @ceraph/react-native-mcp init
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
This automatically:
|
|
24
|
+
- Configures MCP servers for all detected clients (Claude Code, Cursor, Codex, VS Code, Windsurf, Antigravity)
|
|
25
|
+
- Installs a Claude Code hook that injects runtime errors into your conversation automatically
|
|
26
|
+
- Adds `.rn-errors.json` to your `.gitignore`
|
|
27
|
+
|
|
28
|
+
## Manual Setup
|
|
29
|
+
|
|
30
|
+
If you prefer to configure manually, follow the instructions for your client below.
|
|
31
|
+
|
|
32
|
+
### Claude Code
|
|
33
|
+
|
|
34
|
+
Add to `.mcp.json` in your project root:
|
|
35
|
+
|
|
36
|
+
```json
|
|
37
|
+
{
|
|
38
|
+
"mcpServers": {
|
|
39
|
+
"mobile-mcp": {
|
|
40
|
+
"command": "npx",
|
|
41
|
+
"args": ["-y", "@mobilenext/mobile-mcp@latest"]
|
|
42
|
+
},
|
|
43
|
+
"react-native-mcp": {
|
|
44
|
+
"command": "npx",
|
|
45
|
+
"args": ["-y", "@ceraph/react-native-mcp@latest"]
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Cursor
|
|
52
|
+
|
|
53
|
+
Add to `.cursor/mcp.json` in your project root:
|
|
54
|
+
|
|
55
|
+
```json
|
|
56
|
+
{
|
|
57
|
+
"mcpServers": {
|
|
58
|
+
"mobile-mcp": {
|
|
59
|
+
"command": "npx",
|
|
60
|
+
"args": ["-y", "@mobilenext/mobile-mcp@latest"]
|
|
61
|
+
},
|
|
62
|
+
"react-native-mcp": {
|
|
63
|
+
"command": "npx",
|
|
64
|
+
"args": ["-y", "@ceraph/react-native-mcp@latest"]
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Codex
|
|
71
|
+
|
|
72
|
+
Add to `.codex/config.toml` in your project root:
|
|
73
|
+
|
|
74
|
+
```toml
|
|
75
|
+
[mcp_servers.mobile-mcp]
|
|
76
|
+
command = "npx"
|
|
77
|
+
args = ["-y", "@mobilenext/mobile-mcp@latest"]
|
|
78
|
+
|
|
79
|
+
[mcp_servers.react-native-mcp]
|
|
80
|
+
command = "npx"
|
|
81
|
+
args = ["-y", "@ceraph/react-native-mcp@latest"]
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### VS Code / Copilot
|
|
85
|
+
|
|
86
|
+
Add to `.vscode/mcp.json` in your project root:
|
|
87
|
+
|
|
88
|
+
```json
|
|
89
|
+
{
|
|
90
|
+
"mcpServers": {
|
|
91
|
+
"mobile-mcp": {
|
|
92
|
+
"command": "npx",
|
|
93
|
+
"args": ["-y", "@mobilenext/mobile-mcp@latest"]
|
|
94
|
+
},
|
|
95
|
+
"react-native-mcp": {
|
|
96
|
+
"command": "npx",
|
|
97
|
+
"args": ["-y", "@ceraph/react-native-mcp@latest"]
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Windsurf
|
|
104
|
+
|
|
105
|
+
Add to `~/.codeium/windsurf/mcp_config.json` (or Settings → Advanced Settings → Cascade):
|
|
106
|
+
|
|
107
|
+
```json
|
|
108
|
+
{
|
|
109
|
+
"mcpServers": {
|
|
110
|
+
"mobile-mcp": {
|
|
111
|
+
"command": "npx",
|
|
112
|
+
"args": ["-y", "@mobilenext/mobile-mcp@latest"]
|
|
113
|
+
},
|
|
114
|
+
"react-native-mcp": {
|
|
115
|
+
"command": "npx",
|
|
116
|
+
"args": ["-y", "@ceraph/react-native-mcp@latest"]
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Antigravity
|
|
123
|
+
|
|
124
|
+
Add to `~/.gemini/antigravity/mcp_config.json` (or Manage MCP Servers → View raw config):
|
|
125
|
+
|
|
126
|
+
```json
|
|
127
|
+
{
|
|
128
|
+
"mcpServers": {
|
|
129
|
+
"mobile-mcp": {
|
|
130
|
+
"command": "npx",
|
|
131
|
+
"args": ["-y", "@mobilenext/mobile-mcp@latest"]
|
|
132
|
+
},
|
|
133
|
+
"react-native-mcp": {
|
|
134
|
+
"command": "npx",
|
|
135
|
+
"args": ["-y", "@ceraph/react-native-mcp@latest"]
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Cline / Roo Code (VS Code extensions)
|
|
142
|
+
|
|
143
|
+
Open VS Code Settings → Extensions → Cline (or Roo Code) → MCP Servers, then add:
|
|
144
|
+
|
|
145
|
+
```json
|
|
146
|
+
{
|
|
147
|
+
"mobile-mcp": {
|
|
148
|
+
"command": "npx",
|
|
149
|
+
"args": ["-y", "@mobilenext/mobile-mcp@latest"]
|
|
150
|
+
},
|
|
151
|
+
"react-native-mcp": {
|
|
152
|
+
"command": "npx",
|
|
153
|
+
"args": ["-y", "@ceraph/react-native-mcp@latest"]
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### JetBrains IDEs
|
|
159
|
+
|
|
160
|
+
Settings → Tools → MCP Servers → Add, then enter:
|
|
161
|
+
|
|
162
|
+
- **Name:** `react-native-mcp`
|
|
163
|
+
- **Command:** `npx`
|
|
164
|
+
- **Args:** `-y @ceraph/react-native-mcp@latest`
|
|
165
|
+
|
|
166
|
+
Repeat for `mobile-mcp` with args `-y @mobilenext/mobile-mcp@latest`.
|
|
167
|
+
|
|
168
|
+
## Tools
|
|
169
|
+
|
|
170
|
+
### Build & Runtime
|
|
171
|
+
|
|
172
|
+
| Tool | Description |
|
|
173
|
+
|---|---|
|
|
174
|
+
| `rn_build_ios` | Build the app with `expo run:ios`. Captures Xcode output and returns structured errors (file, line, message, type). Optionally runs `prebuild --clean` first. |
|
|
175
|
+
| `rn_start` | Start Metro dev server. Monitors console output for runtime errors, JS exceptions, and red screens. |
|
|
176
|
+
| `rn_get_errors` | Return all captured build and runtime errors without re-running anything. |
|
|
177
|
+
| `rn_get_console` | Return recent Metro console output, filtered by log level. |
|
|
178
|
+
| `rn_check_prebuild` | Detect if `prebuild --clean` is needed by diffing `package.json`, `app.json`, and `Podfile.lock` against the last successful build. |
|
|
179
|
+
| `rn_stop` | Kill all managed React Native processes. |
|
|
180
|
+
|
|
181
|
+
### Screen Interaction
|
|
182
|
+
|
|
183
|
+
| Tool | Description |
|
|
184
|
+
|---|---|
|
|
185
|
+
| `screen_tap` | Tap at coordinates with automatic pixel ratio correction. Screenshot coordinates are divided by the device pixel ratio (2x/3x) so taps land where you expect. |
|
|
186
|
+
| `screen_find_and_tap` | Find an element by text, accessibility label, or type, then tap its center. Most reliable interaction method — no coordinate guessing. |
|
|
187
|
+
|
|
188
|
+
## Why both MCPs?
|
|
189
|
+
|
|
190
|
+
`mobile-mcp` handles low-level device interaction (screenshots, swipe, app lifecycle, recording) and is actively maintained for iOS/Android compatibility.
|
|
191
|
+
|
|
192
|
+
`@ceraph/react-native-mcp` adds the development workflow layer on top: structured error capture, console monitoring, pixel-ratio-corrected taps, and prebuild detection. Both talk to WebDriverAgent independently — they don't depend on each other, they complement each other.
|
|
193
|
+
|
|
194
|
+
## License
|
|
195
|
+
|
|
196
|
+
MIT
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @ceraph/react-native-mcp CLI entry point.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* npx @ceraph/react-native-mcp init — set up MCP config and error hook
|
|
7
|
+
* npx @ceraph/react-native-mcp — start the MCP server (stdio transport)
|
|
8
|
+
*/
|
|
9
|
+
const command = process.argv[2];
|
|
10
|
+
if (command === "init") {
|
|
11
|
+
await import("./init.js");
|
|
12
|
+
}
|
|
13
|
+
else {
|
|
14
|
+
await import("./index.js");
|
|
15
|
+
}
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parses Xcode build errors, Metro bundler errors, and runtime errors
|
|
3
|
+
* from raw process output.
|
|
4
|
+
*/
|
|
5
|
+
export interface BuildError {
|
|
6
|
+
file: string;
|
|
7
|
+
line: number;
|
|
8
|
+
column: number;
|
|
9
|
+
message: string;
|
|
10
|
+
severity: "error" | "warning" | "note";
|
|
11
|
+
type: "build" | "link" | "pod";
|
|
12
|
+
}
|
|
13
|
+
export interface RuntimeError {
|
|
14
|
+
message: string;
|
|
15
|
+
stack: string;
|
|
16
|
+
timestamp: string;
|
|
17
|
+
}
|
|
18
|
+
export interface Warning {
|
|
19
|
+
message: string;
|
|
20
|
+
source: string;
|
|
21
|
+
}
|
|
22
|
+
export interface AllErrors {
|
|
23
|
+
buildErrors: BuildError[];
|
|
24
|
+
runtimeErrors: RuntimeError[];
|
|
25
|
+
warnings: Warning[];
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Parse all build output lines and extract structured errors.
|
|
29
|
+
*/
|
|
30
|
+
export declare function parseBuildOutput(lines: string[]): {
|
|
31
|
+
errors: BuildError[];
|
|
32
|
+
warnings: Warning[];
|
|
33
|
+
buildFailed: boolean;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Stateful parser for Metro output. Multi-line errors (message + stack
|
|
37
|
+
* trace) arrive across multiple `onData` chunks, so the in-progress
|
|
38
|
+
* error must persist across calls. Use one instance per Metro session
|
|
39
|
+
* and call `parse(lines)` as chunks arrive; call `flush()` at the end
|
|
40
|
+
* (e.g. on process exit) to emit any error still being assembled.
|
|
41
|
+
*/
|
|
42
|
+
export declare class MetroErrorParser {
|
|
43
|
+
private currentStack;
|
|
44
|
+
private currentErrorMessage;
|
|
45
|
+
private inStackTrace;
|
|
46
|
+
parse(lines: string[]): {
|
|
47
|
+
runtimeErrors: RuntimeError[];
|
|
48
|
+
warnings: Warning[];
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Force-emit any error currently being assembled. Call on stream end.
|
|
52
|
+
*/
|
|
53
|
+
flush(): {
|
|
54
|
+
runtimeErrors: RuntimeError[];
|
|
55
|
+
warnings: Warning[];
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Parse a complete Metro buffer in one shot. Suitable for batch use
|
|
60
|
+
* (e.g. `getErrors()` re-parsing the rolling buffer); for streaming use
|
|
61
|
+
* a `MetroErrorParser` instance instead so multi-line state survives
|
|
62
|
+
* across chunks.
|
|
63
|
+
*/
|
|
64
|
+
export declare function parseMetroOutput(lines: string[]): {
|
|
65
|
+
runtimeErrors: RuntimeError[];
|
|
66
|
+
warnings: Warning[];
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* Classify a log line by level.
|
|
70
|
+
*/
|
|
71
|
+
export declare function classifyLogLevel(line: string): "error" | "warn" | "log";
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parses Xcode build errors, Metro bundler errors, and runtime errors
|
|
3
|
+
* from raw process output.
|
|
4
|
+
*/
|
|
5
|
+
// Xcode-style: /path/to/File.swift:12:5: error: something went wrong
|
|
6
|
+
const XCODE_ERROR_RE = /^(.+?):(\d+):(\d+):\s*(error|warning|note):\s*(.+)$/;
|
|
7
|
+
// Xcode build failure markers
|
|
8
|
+
const BUILD_FAILED_MARKERS = [
|
|
9
|
+
"Build Failed",
|
|
10
|
+
"** BUILD FAILED **",
|
|
11
|
+
"xcodebuild: error:",
|
|
12
|
+
"❌",
|
|
13
|
+
];
|
|
14
|
+
// Linker errors
|
|
15
|
+
const LINKER_ERROR_RE = /^(ld|clang):\s*(error|warning):\s*(.+)$/;
|
|
16
|
+
const UNDEFINED_SYMBOL_RE = /^Undefined symbols? for architecture .+:\s*"(.+)"/;
|
|
17
|
+
// CocoaPods errors
|
|
18
|
+
const POD_ERROR_RE = /^\[!\]\s*(.+)$/;
|
|
19
|
+
// Metro bundler errors
|
|
20
|
+
const METRO_ERROR_RE = /^(Error|TypeError|ReferenceError|SyntaxError|RangeError):\s*(.+)$/;
|
|
21
|
+
const METRO_MODULE_RE = /Unable to resolve module ['"](.+?)['"]/;
|
|
22
|
+
const METRO_ENCOUNTERED_RE = /Metro has encountered an error/;
|
|
23
|
+
// Runtime errors from console
|
|
24
|
+
const CONSOLE_ERROR_RE = /^(ERROR|console\.error)\s*(.+)$/i;
|
|
25
|
+
const INVARIANT_RE = /Invariant Violation:\s*(.+)/;
|
|
26
|
+
const UNHANDLED_JS_RE = /Unhandled JS Exception:\s*(.+)/;
|
|
27
|
+
const RED_SCREEN_RE = /ExceptionsManager\.js/;
|
|
28
|
+
/**
|
|
29
|
+
* Parse a single line for Xcode build errors/warnings.
|
|
30
|
+
*/
|
|
31
|
+
function parseXcodeLine(line) {
|
|
32
|
+
const match = line.match(XCODE_ERROR_RE);
|
|
33
|
+
if (match) {
|
|
34
|
+
return {
|
|
35
|
+
file: match[1],
|
|
36
|
+
line: parseInt(match[2], 10),
|
|
37
|
+
column: parseInt(match[3], 10),
|
|
38
|
+
message: match[5],
|
|
39
|
+
severity: match[4],
|
|
40
|
+
type: "build",
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Parse a single line for linker errors.
|
|
47
|
+
*/
|
|
48
|
+
function parseLinkerLine(line) {
|
|
49
|
+
const linkerMatch = line.match(LINKER_ERROR_RE);
|
|
50
|
+
if (linkerMatch) {
|
|
51
|
+
return {
|
|
52
|
+
file: "",
|
|
53
|
+
line: 0,
|
|
54
|
+
column: 0,
|
|
55
|
+
message: linkerMatch[3],
|
|
56
|
+
severity: linkerMatch[2],
|
|
57
|
+
type: "link",
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
const undefinedMatch = line.match(UNDEFINED_SYMBOL_RE);
|
|
61
|
+
if (undefinedMatch) {
|
|
62
|
+
return {
|
|
63
|
+
file: "",
|
|
64
|
+
line: 0,
|
|
65
|
+
column: 0,
|
|
66
|
+
message: `Undefined symbol: ${undefinedMatch[1]}`,
|
|
67
|
+
severity: "error",
|
|
68
|
+
type: "link",
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Parse a single line for CocoaPods notices.
|
|
75
|
+
*
|
|
76
|
+
* CocoaPods uses the `[!]` prefix for both errors and warnings. Default
|
|
77
|
+
* to `error` and only downgrade when the message matches a known-safe
|
|
78
|
+
* pattern (target overrides, deprecation hints, CDN/repo update notices,
|
|
79
|
+
* advisory "should" / "recommends" copy). This is closed under unknown
|
|
80
|
+
* failure modes — a new pod error vocabulary stays classified as an error
|
|
81
|
+
* rather than silently becoming a warning that lets a broken build
|
|
82
|
+
* resolve as success.
|
|
83
|
+
*/
|
|
84
|
+
const POD_SAFE_WARNING_RE = /\b(cdn|trunk|repo update|overrides the|setting defined in|deprecated|recommends|should)\b/i;
|
|
85
|
+
function parsePodLine(line) {
|
|
86
|
+
const match = line.match(POD_ERROR_RE);
|
|
87
|
+
if (!match)
|
|
88
|
+
return null;
|
|
89
|
+
const message = match[1];
|
|
90
|
+
const isSafeWarning = POD_SAFE_WARNING_RE.test(message);
|
|
91
|
+
return {
|
|
92
|
+
file: "Podfile",
|
|
93
|
+
line: 0,
|
|
94
|
+
column: 0,
|
|
95
|
+
message,
|
|
96
|
+
severity: isSafeWarning ? "warning" : "error",
|
|
97
|
+
type: "pod",
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Parse all build output lines and extract structured errors.
|
|
102
|
+
*/
|
|
103
|
+
export function parseBuildOutput(lines) {
|
|
104
|
+
const errors = [];
|
|
105
|
+
const warnings = [];
|
|
106
|
+
let buildFailed = false;
|
|
107
|
+
// Track multi-line error context: sometimes Xcode emits the error
|
|
108
|
+
// on one line and the source context on the next few lines.
|
|
109
|
+
// We append context to the last error's message.
|
|
110
|
+
let lastError = null;
|
|
111
|
+
let contextLines = 0;
|
|
112
|
+
const MAX_CONTEXT = 3;
|
|
113
|
+
for (const line of lines) {
|
|
114
|
+
const trimmed = line.trim();
|
|
115
|
+
if (!trimmed) {
|
|
116
|
+
lastError = null;
|
|
117
|
+
contextLines = 0;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
// Check for build failure markers
|
|
121
|
+
if (BUILD_FAILED_MARKERS.some((m) => trimmed.includes(m))) {
|
|
122
|
+
buildFailed = true;
|
|
123
|
+
}
|
|
124
|
+
// Try Xcode error format
|
|
125
|
+
const xcodeError = parseXcodeLine(trimmed);
|
|
126
|
+
if (xcodeError) {
|
|
127
|
+
if (xcodeError.severity === "error") {
|
|
128
|
+
errors.push(xcodeError);
|
|
129
|
+
lastError = xcodeError;
|
|
130
|
+
contextLines = 0;
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
warnings.push({
|
|
134
|
+
message: xcodeError.message,
|
|
135
|
+
source: `${xcodeError.file}:${xcodeError.line}:${xcodeError.column}`,
|
|
136
|
+
});
|
|
137
|
+
// Don't set lastError for warnings/notes
|
|
138
|
+
}
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
// Try linker error format
|
|
142
|
+
const linkerError = parseLinkerLine(trimmed);
|
|
143
|
+
if (linkerError) {
|
|
144
|
+
if (linkerError.severity === "warning") {
|
|
145
|
+
warnings.push({ message: linkerError.message, source: "linker" });
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
errors.push(linkerError);
|
|
149
|
+
}
|
|
150
|
+
lastError = linkerError;
|
|
151
|
+
contextLines = 0;
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
// Try CocoaPods notice format (warning or error depending on message)
|
|
155
|
+
const podNotice = parsePodLine(trimmed);
|
|
156
|
+
if (podNotice) {
|
|
157
|
+
if (podNotice.severity === "warning") {
|
|
158
|
+
warnings.push({ message: podNotice.message, source: "CocoaPods" });
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
errors.push(podNotice);
|
|
162
|
+
lastError = podNotice;
|
|
163
|
+
contextLines = 0;
|
|
164
|
+
}
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
// Append context lines to last error for multi-line messages
|
|
168
|
+
if (lastError && contextLines < MAX_CONTEXT) {
|
|
169
|
+
lastError.message += `\n${trimmed}`;
|
|
170
|
+
contextLines++;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return { errors, warnings, buildFailed };
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Stateful parser for Metro output. Multi-line errors (message + stack
|
|
177
|
+
* trace) arrive across multiple `onData` chunks, so the in-progress
|
|
178
|
+
* error must persist across calls. Use one instance per Metro session
|
|
179
|
+
* and call `parse(lines)` as chunks arrive; call `flush()` at the end
|
|
180
|
+
* (e.g. on process exit) to emit any error still being assembled.
|
|
181
|
+
*/
|
|
182
|
+
export class MetroErrorParser {
|
|
183
|
+
currentStack = [];
|
|
184
|
+
currentErrorMessage = "";
|
|
185
|
+
inStackTrace = false;
|
|
186
|
+
parse(lines) {
|
|
187
|
+
const runtimeErrors = [];
|
|
188
|
+
const warnings = [];
|
|
189
|
+
const flushInto = (sink) => {
|
|
190
|
+
if (this.currentErrorMessage) {
|
|
191
|
+
sink.push({
|
|
192
|
+
message: this.currentErrorMessage,
|
|
193
|
+
stack: this.currentStack.join("\n"),
|
|
194
|
+
timestamp: new Date().toISOString(),
|
|
195
|
+
});
|
|
196
|
+
this.currentErrorMessage = "";
|
|
197
|
+
this.currentStack = [];
|
|
198
|
+
this.inStackTrace = false;
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
for (const line of lines) {
|
|
202
|
+
const trimmed = line.trim();
|
|
203
|
+
// Metro module resolution errors — single-line, emit immediately.
|
|
204
|
+
const moduleMatch = trimmed.match(METRO_MODULE_RE);
|
|
205
|
+
if (moduleMatch) {
|
|
206
|
+
flushInto(runtimeErrors);
|
|
207
|
+
runtimeErrors.push({
|
|
208
|
+
message: `Unable to resolve module '${moduleMatch[1]}'`,
|
|
209
|
+
stack: trimmed,
|
|
210
|
+
timestamp: new Date().toISOString(),
|
|
211
|
+
});
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
// Metro encountered an error
|
|
215
|
+
if (METRO_ENCOUNTERED_RE.test(trimmed)) {
|
|
216
|
+
flushInto(runtimeErrors);
|
|
217
|
+
this.currentErrorMessage = "Metro has encountered an error";
|
|
218
|
+
this.inStackTrace = true;
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
// Invariant Violation
|
|
222
|
+
const invariantMatch = trimmed.match(INVARIANT_RE);
|
|
223
|
+
if (invariantMatch) {
|
|
224
|
+
flushInto(runtimeErrors);
|
|
225
|
+
this.currentErrorMessage = `Invariant Violation: ${invariantMatch[1]}`;
|
|
226
|
+
this.inStackTrace = true;
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
// Unhandled JS Exception
|
|
230
|
+
const unhandledMatch = trimmed.match(UNHANDLED_JS_RE);
|
|
231
|
+
if (unhandledMatch) {
|
|
232
|
+
flushInto(runtimeErrors);
|
|
233
|
+
this.currentErrorMessage = `Unhandled JS Exception: ${unhandledMatch[1]}`;
|
|
234
|
+
this.inStackTrace = true;
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
// JS error types
|
|
238
|
+
const metroMatch = trimmed.match(METRO_ERROR_RE);
|
|
239
|
+
if (metroMatch) {
|
|
240
|
+
flushInto(runtimeErrors);
|
|
241
|
+
this.currentErrorMessage = `${metroMatch[1]}: ${metroMatch[2]}`;
|
|
242
|
+
this.inStackTrace = true;
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
// Console errors
|
|
246
|
+
const consoleMatch = trimmed.match(CONSOLE_ERROR_RE);
|
|
247
|
+
if (consoleMatch) {
|
|
248
|
+
flushInto(runtimeErrors);
|
|
249
|
+
this.currentErrorMessage = consoleMatch[2];
|
|
250
|
+
this.inStackTrace = true;
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
// Red screen detection
|
|
254
|
+
if (RED_SCREEN_RE.test(trimmed)) {
|
|
255
|
+
if (!this.currentErrorMessage) {
|
|
256
|
+
this.currentErrorMessage = "Red screen error detected";
|
|
257
|
+
}
|
|
258
|
+
this.inStackTrace = true;
|
|
259
|
+
}
|
|
260
|
+
// Stack trace lines (indented, or starting with "at ")
|
|
261
|
+
if (this.inStackTrace) {
|
|
262
|
+
if (trimmed.startsWith("at ") ||
|
|
263
|
+
trimmed.startsWith("in ") ||
|
|
264
|
+
/^\s+at\s/.test(line) ||
|
|
265
|
+
/^\d+\s*\|/.test(trimmed) // source code context lines
|
|
266
|
+
) {
|
|
267
|
+
this.currentStack.push(trimmed);
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
else if (trimmed === "") {
|
|
271
|
+
flushInto(runtimeErrors);
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
// Continuation of the error message. Skip the warning check —
|
|
276
|
+
// a `warn ...` line interleaved with a stack trace would
|
|
277
|
+
// otherwise be double-counted as both stack and warning.
|
|
278
|
+
this.currentStack.push(trimmed);
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
// Warnings from Metro
|
|
283
|
+
if (/^warn\s/i.test(trimmed) || /\(WARN\)/.test(trimmed)) {
|
|
284
|
+
warnings.push({
|
|
285
|
+
message: trimmed.replace(/^warn\s*/i, "").replace(/\(WARN\)\s*/, ""),
|
|
286
|
+
source: "metro",
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
// Do NOT flush at end of chunk — the in-progress error may continue
|
|
291
|
+
// in the next chunk. Caller must invoke `flush()` when the stream ends.
|
|
292
|
+
return { runtimeErrors, warnings };
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Force-emit any error currently being assembled. Call on stream end.
|
|
296
|
+
*/
|
|
297
|
+
flush() {
|
|
298
|
+
const runtimeErrors = [];
|
|
299
|
+
if (this.currentErrorMessage) {
|
|
300
|
+
runtimeErrors.push({
|
|
301
|
+
message: this.currentErrorMessage,
|
|
302
|
+
stack: this.currentStack.join("\n"),
|
|
303
|
+
timestamp: new Date().toISOString(),
|
|
304
|
+
});
|
|
305
|
+
this.currentErrorMessage = "";
|
|
306
|
+
this.currentStack = [];
|
|
307
|
+
this.inStackTrace = false;
|
|
308
|
+
}
|
|
309
|
+
return { runtimeErrors, warnings: [] };
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Parse a complete Metro buffer in one shot. Suitable for batch use
|
|
314
|
+
* (e.g. `getErrors()` re-parsing the rolling buffer); for streaming use
|
|
315
|
+
* a `MetroErrorParser` instance instead so multi-line state survives
|
|
316
|
+
* across chunks.
|
|
317
|
+
*/
|
|
318
|
+
export function parseMetroOutput(lines) {
|
|
319
|
+
const parser = new MetroErrorParser();
|
|
320
|
+
const streamed = parser.parse(lines);
|
|
321
|
+
const flushed = parser.flush();
|
|
322
|
+
return {
|
|
323
|
+
runtimeErrors: [...streamed.runtimeErrors, ...flushed.runtimeErrors],
|
|
324
|
+
warnings: [...streamed.warnings, ...flushed.warnings],
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Classify a log line by level.
|
|
329
|
+
*/
|
|
330
|
+
export function classifyLogLevel(line) {
|
|
331
|
+
const trimmed = line.trim().toLowerCase();
|
|
332
|
+
if (trimmed.startsWith("error") ||
|
|
333
|
+
trimmed.includes("console.error") ||
|
|
334
|
+
trimmed.startsWith("❌") ||
|
|
335
|
+
/^\s*error\s*:/i.test(line)) {
|
|
336
|
+
return "error";
|
|
337
|
+
}
|
|
338
|
+
if (trimmed.startsWith("warn") ||
|
|
339
|
+
trimmed.includes("console.warn") ||
|
|
340
|
+
trimmed.includes("(warn)") ||
|
|
341
|
+
/^\s*warning\s*:/i.test(line)) {
|
|
342
|
+
return "warn";
|
|
343
|
+
}
|
|
344
|
+
return "log";
|
|
345
|
+
}
|