@ceraph/react-native-mcp 0.2.1 → 0.3.1

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.
Files changed (124) hide show
  1. package/LICENSE +116 -15
  2. package/README.md +79 -77
  3. package/assets/default.png +0 -0
  4. package/dist/app-lifecycle.d.ts +50 -0
  5. package/dist/app-lifecycle.js +487 -0
  6. package/dist/camera-image-writer.d.ts +43 -0
  7. package/dist/camera-image-writer.js +280 -0
  8. package/dist/camera-registry-sync.d.ts +18 -0
  9. package/dist/camera-registry-sync.js +117 -0
  10. package/dist/cli.d.ts +0 -7
  11. package/dist/cli.js +41 -9
  12. package/dist/device-autonomy.d.ts +30 -0
  13. package/dist/device-autonomy.js +117 -0
  14. package/dist/error-parser.d.ts +6 -26
  15. package/dist/error-parser.js +4 -74
  16. package/dist/expo-manager.d.ts +2 -74
  17. package/dist/expo-manager.js +11 -125
  18. package/dist/index.d.ts +0 -7
  19. package/dist/index.js +1266 -56
  20. package/dist/init/ast-camera.d.ts +29 -0
  21. package/dist/init/ast-camera.js +267 -0
  22. package/dist/init/ast-layout.d.ts +15 -0
  23. package/dist/init/ast-layout.js +167 -0
  24. package/dist/init/claude-hook-constants.d.ts +9 -0
  25. package/dist/init/claude-hook-constants.js +91 -0
  26. package/dist/init/lan-ip.d.ts +11 -0
  27. package/dist/init/lan-ip.js +51 -0
  28. package/dist/init/monorepo.d.ts +13 -0
  29. package/dist/init/monorepo.js +185 -0
  30. package/dist/init/oauth.d.ts +52 -0
  31. package/dist/init/oauth.js +220 -0
  32. package/dist/init/package-manager.d.ts +11 -0
  33. package/dist/init/package-manager.js +60 -0
  34. package/dist/init/prompt.d.ts +12 -0
  35. package/dist/init/prompt.js +68 -0
  36. package/dist/init/shell-profile.d.ts +22 -0
  37. package/dist/init/shell-profile.js +85 -0
  38. package/dist/init/steps.d.ts +135 -0
  39. package/dist/init/steps.js +399 -0
  40. package/dist/init/url-scheme.d.ts +42 -0
  41. package/dist/init/url-scheme.js +187 -0
  42. package/dist/init/walkthrough.d.ts +76 -0
  43. package/dist/init/walkthrough.js +340 -0
  44. package/dist/init.d.ts +7 -7
  45. package/dist/init.js +280 -120
  46. package/dist/iproxy-manager.d.ts +32 -0
  47. package/dist/iproxy-manager.js +216 -0
  48. package/dist/mac-caffeinate.d.ts +10 -0
  49. package/dist/mac-caffeinate.js +56 -0
  50. package/dist/permission-interceptor.d.ts +29 -0
  51. package/dist/permission-interceptor.js +185 -0
  52. package/dist/prebuild-detector.d.ts +0 -30
  53. package/dist/prebuild-detector.js +1 -42
  54. package/dist/preflight.d.ts +34 -0
  55. package/dist/preflight.js +847 -0
  56. package/dist/screen.d.ts +132 -43
  57. package/dist/screen.js +668 -94
  58. package/dist/shim/boot.d.ts +41 -0
  59. package/dist/shim/boot.js +141 -0
  60. package/dist/shim/camera.d.ts +22 -0
  61. package/dist/shim/camera.js +62 -0
  62. package/dist/shim/config.d.ts +6 -0
  63. package/dist/shim/config.js +56 -0
  64. package/dist/shim/deep-link.d.ts +1 -0
  65. package/dist/shim/deep-link.js +25 -0
  66. package/dist/shim/dev-guard.d.ts +1 -0
  67. package/dist/shim/dev-guard.js +3 -0
  68. package/dist/shim/error-handler.d.ts +20 -0
  69. package/dist/shim/error-handler.js +66 -0
  70. package/dist/shim/fetch-interceptor.d.ts +13 -0
  71. package/dist/shim/fetch-interceptor.js +93 -0
  72. package/dist/shim/index.d.ts +6 -0
  73. package/dist/shim/index.js +6 -0
  74. package/dist/shim/keep-awake.d.ts +13 -0
  75. package/dist/shim/keep-awake.js +118 -0
  76. package/dist/shim/reload.d.ts +23 -0
  77. package/dist/shim/reload.js +76 -0
  78. package/dist/shim/signal-capture.d.ts +11 -0
  79. package/dist/shim/signal-capture.js +15 -0
  80. package/dist/shim/signal-transport.d.ts +17 -0
  81. package/dist/shim/signal-transport.js +43 -0
  82. package/dist/signal-listener.d.ts +27 -0
  83. package/dist/signal-listener.js +135 -0
  84. package/dist/simulator-boot.d.ts +52 -0
  85. package/dist/simulator-boot.js +227 -0
  86. package/dist/target.d.ts +48 -0
  87. package/dist/target.js +267 -0
  88. package/dist/uninstall/cli-runner.d.ts +32 -0
  89. package/dist/uninstall/cli-runner.js +223 -0
  90. package/dist/uninstall/footprint.d.ts +40 -0
  91. package/dist/uninstall/footprint.js +288 -0
  92. package/dist/uninstall/mcp-tools.d.ts +14 -0
  93. package/dist/uninstall/mcp-tools.js +175 -0
  94. package/dist/uninstall/revert-auth.d.ts +22 -0
  95. package/dist/uninstall/revert-auth.js +31 -0
  96. package/dist/uninstall/revert-boot.d.ts +24 -0
  97. package/dist/uninstall/revert-boot.js +242 -0
  98. package/dist/uninstall/revert-camera.d.ts +12 -0
  99. package/dist/uninstall/revert-camera.js +199 -0
  100. package/dist/uninstall/revert-ceraph-dir.d.ts +27 -0
  101. package/dist/uninstall/revert-ceraph-dir.js +38 -0
  102. package/dist/uninstall/revert-claude-hooks.d.ts +19 -0
  103. package/dist/uninstall/revert-claude-hooks.js +191 -0
  104. package/dist/uninstall/revert-gitignore.d.ts +17 -0
  105. package/dist/uninstall/revert-gitignore.js +43 -0
  106. package/dist/uninstall/revert-mcp-clients.d.ts +57 -0
  107. package/dist/uninstall/revert-mcp-clients.js +194 -0
  108. package/dist/uninstall/revert-package.d.ts +34 -0
  109. package/dist/uninstall/revert-package.js +98 -0
  110. package/dist/uninstall/revert-scheme.d.ts +36 -0
  111. package/dist/uninstall/revert-scheme.js +139 -0
  112. package/dist/uninstall/revert-signal-host-env.d.ts +31 -0
  113. package/dist/uninstall/revert-signal-host-env.js +61 -0
  114. package/dist/uninstall/walkthrough.d.ts +80 -0
  115. package/dist/uninstall/walkthrough.js +1244 -0
  116. package/dist/utils/atomic-write.d.ts +1 -0
  117. package/dist/utils/atomic-write.js +30 -0
  118. package/dist/wait-for-device.d.ts +68 -0
  119. package/dist/wait-for-device.js +368 -0
  120. package/dist/wda-manager.d.ts +38 -0
  121. package/dist/wda-manager.js +186 -0
  122. package/dist/wda-simulator.d.ts +28 -0
  123. package/dist/wda-simulator.js +257 -0
  124. package/package.json +38 -5
@@ -0,0 +1,216 @@
1
+ import { spawn } from "node:child_process";
2
+ const DEFAULT_LOCAL = 8100;
3
+ const DEFAULT_DEVICE = 8100;
4
+ const MAX_LOG_LINES = 50;
5
+ function defaultWhich(bin) {
6
+ return new Promise((resolve) => {
7
+ let stdout = "";
8
+ let settled = false;
9
+ const child = spawn("which", [bin], {
10
+ stdio: ["ignore", "pipe", "ignore"],
11
+ });
12
+ child.stdout.on("data", (chunk) => {
13
+ stdout += chunk.toString();
14
+ });
15
+ child.on("error", () => {
16
+ if (!settled) {
17
+ settled = true;
18
+ resolve(null);
19
+ }
20
+ });
21
+ child.on("exit", (code) => {
22
+ if (settled)
23
+ return;
24
+ settled = true;
25
+ if (code !== 0)
26
+ resolve(null);
27
+ else {
28
+ const trimmed = stdout.trim();
29
+ resolve(trimmed.length > 0 ? trimmed : null);
30
+ }
31
+ });
32
+ });
33
+ }
34
+ export class IproxyManager {
35
+ child = null;
36
+ udid = null;
37
+ localPort;
38
+ devicePort;
39
+ stderrBuffer = [];
40
+ spawnFn;
41
+ whichFn;
42
+ startInFlight = null;
43
+ constructor(opts = {}) {
44
+ this.localPort = opts.localPort ?? DEFAULT_LOCAL;
45
+ this.devicePort = opts.devicePort ?? DEFAULT_DEVICE;
46
+ this.spawnFn = opts.spawnFn ?? spawn;
47
+ this.whichFn = opts.whichFn ?? defaultWhich;
48
+ }
49
+ isRunning() {
50
+ return this.child !== null && this.child.exitCode === null;
51
+ }
52
+ async start(udid) {
53
+ while (true) {
54
+ if (this.isRunning()) {
55
+ return {
56
+ ok: false,
57
+ reason: "already-running",
58
+ pid: this.child?.pid,
59
+ };
60
+ }
61
+ if (!this.startInFlight)
62
+ break;
63
+ await this.startInFlight.catch(() => undefined);
64
+ }
65
+ const myInFlight = this.startInner(udid);
66
+ this.startInFlight = myInFlight;
67
+ try {
68
+ return await myInFlight;
69
+ }
70
+ finally {
71
+ if (this.startInFlight === myInFlight) {
72
+ this.startInFlight = null;
73
+ }
74
+ }
75
+ }
76
+ async startInner(udid) {
77
+ if (this.isRunning()) {
78
+ return {
79
+ ok: false,
80
+ reason: "already-running",
81
+ pid: this.child?.pid,
82
+ };
83
+ }
84
+ const binPath = await this.whichFn("iproxy");
85
+ if (!binPath) {
86
+ return { ok: false, reason: "not-installed" };
87
+ }
88
+ this.udid = udid;
89
+ this.stderrBuffer = [];
90
+ let child;
91
+ try {
92
+ child = this.spawnFn(binPath, [
93
+ String(this.localPort),
94
+ String(this.devicePort),
95
+ "-u",
96
+ udid,
97
+ ], { stdio: ["ignore", "pipe", "pipe"] });
98
+ }
99
+ catch (err) {
100
+ this.appendStderr(`iproxy spawn threw: ${err instanceof Error ? err.message : String(err)}`);
101
+ return { ok: false, reason: "spawn-failed", stderr: [...this.stderrBuffer] };
102
+ }
103
+ child.stderr?.on("data", (chunk) => {
104
+ const text = chunk.toString();
105
+ for (const line of text.split(/\r?\n/)) {
106
+ if (line.trim().length === 0)
107
+ continue;
108
+ this.appendStderr(line);
109
+ }
110
+ });
111
+ child.stdout?.on("data", (chunk) => {
112
+ const text = chunk.toString();
113
+ for (const line of text.split(/\r?\n/)) {
114
+ if (line.trim().length === 0)
115
+ continue;
116
+ this.appendStderr(`[stdout] ${line}`);
117
+ }
118
+ });
119
+ const earlyExit = await this.detectEarlyExit(child, 50);
120
+ if (earlyExit) {
121
+ this.child = null;
122
+ return {
123
+ ok: false,
124
+ reason: "spawn-failed",
125
+ stderr: [...this.stderrBuffer],
126
+ };
127
+ }
128
+ this.child = child;
129
+ child.on("exit", () => {
130
+ if (this.child === child) {
131
+ this.child = null;
132
+ }
133
+ });
134
+ return {
135
+ ok: true,
136
+ pid: child.pid,
137
+ stderr: [...this.stderrBuffer],
138
+ };
139
+ }
140
+ async stop() {
141
+ if (this.startInFlight) {
142
+ await this.startInFlight.catch(() => undefined);
143
+ }
144
+ const child = this.child;
145
+ if (!child)
146
+ return;
147
+ this.child = null;
148
+ if (child.exitCode !== null)
149
+ return;
150
+ return new Promise((resolve) => {
151
+ let settled = false;
152
+ let killTimer = null;
153
+ let fallbackTimer = null;
154
+ const finish = () => {
155
+ if (settled)
156
+ return;
157
+ settled = true;
158
+ if (killTimer)
159
+ clearTimeout(killTimer);
160
+ if (fallbackTimer)
161
+ clearTimeout(fallbackTimer);
162
+ resolve();
163
+ };
164
+ child.once("exit", finish);
165
+ try {
166
+ child.kill("SIGTERM");
167
+ }
168
+ catch {
169
+ finish();
170
+ return;
171
+ }
172
+ killTimer = setTimeout(() => {
173
+ if (settled)
174
+ return;
175
+ try {
176
+ child.kill("SIGKILL");
177
+ }
178
+ catch {
179
+ }
180
+ fallbackTimer = setTimeout(finish, 100);
181
+ fallbackTimer.unref();
182
+ }, 1000);
183
+ killTimer.unref();
184
+ });
185
+ }
186
+ async restart(udid) {
187
+ await this.stop();
188
+ return this.start(udid ?? this.udid ?? "");
189
+ }
190
+ getStderr() {
191
+ return [...this.stderrBuffer];
192
+ }
193
+ appendStderr(line) {
194
+ this.stderrBuffer.push(line);
195
+ if (this.stderrBuffer.length > MAX_LOG_LINES) {
196
+ this.stderrBuffer.shift();
197
+ }
198
+ }
199
+ detectEarlyExit(child, withinMs) {
200
+ return new Promise((resolve) => {
201
+ let settled = false;
202
+ const finish = (value) => {
203
+ if (settled)
204
+ return;
205
+ settled = true;
206
+ resolve(value);
207
+ };
208
+ const onExit = () => finish(true);
209
+ child.once("exit", onExit);
210
+ setTimeout(() => {
211
+ child.off("exit", onExit);
212
+ finish(false);
213
+ }, withinMs).unref();
214
+ });
215
+ }
216
+ }
@@ -0,0 +1,10 @@
1
+ import { spawn as nodeSpawn } from "node:child_process";
2
+ export interface CaffeinateResult {
3
+ applied: boolean;
4
+ reason: "started" | "already-active" | "not-darwin" | "spawn-failed";
5
+ }
6
+ export type Spawner = typeof nodeSpawn;
7
+ export declare function _resetForTesting(): void;
8
+ export declare function _setSpawnerForTesting(fn: Spawner | null): void;
9
+ export declare function enableCaffeinate(): CaffeinateResult;
10
+ export declare function disableCaffeinate(): void;
@@ -0,0 +1,56 @@
1
+ import { spawn as nodeSpawn } from "node:child_process";
2
+ let child = null;
3
+ let active = false;
4
+ let spawnerOverride = null;
5
+ export function _resetForTesting() {
6
+ child = null;
7
+ active = false;
8
+ spawnerOverride = null;
9
+ }
10
+ export function _setSpawnerForTesting(fn) {
11
+ spawnerOverride = fn;
12
+ }
13
+ export function enableCaffeinate() {
14
+ if (process.platform !== "darwin") {
15
+ return { applied: false, reason: "not-darwin" };
16
+ }
17
+ if (active && child && !child.killed) {
18
+ return { applied: true, reason: "already-active" };
19
+ }
20
+ try {
21
+ const spawnFn = spawnerOverride ?? nodeSpawn;
22
+ const proc = spawnFn("caffeinate", ["-di", "-w", String(process.pid)], {
23
+ detached: true,
24
+ stdio: "ignore",
25
+ });
26
+ if (typeof proc.unref === "function") {
27
+ proc.unref();
28
+ }
29
+ child = proc;
30
+ active = true;
31
+ const thisChild = proc;
32
+ proc.once("exit", () => {
33
+ if (child === thisChild) {
34
+ child = null;
35
+ active = false;
36
+ }
37
+ });
38
+ return { applied: true, reason: "started" };
39
+ }
40
+ catch {
41
+ return { applied: false, reason: "spawn-failed" };
42
+ }
43
+ }
44
+ export function disableCaffeinate() {
45
+ if (!active)
46
+ return;
47
+ if (child) {
48
+ try {
49
+ child.kill("SIGTERM");
50
+ }
51
+ catch {
52
+ }
53
+ }
54
+ child = null;
55
+ active = false;
56
+ }
@@ -0,0 +1,29 @@
1
+ import type { ScreenManager } from "./screen.js";
2
+ export type InterceptorMode = "auto-accept" | "ask" | "off";
3
+ export interface PermissionInterceptorOpts {
4
+ screen: ScreenManager;
5
+ mode: InterceptorMode;
6
+ onDialog?: (info: {
7
+ title: string;
8
+ body: string;
9
+ }) => Promise<"accept" | "deny">;
10
+ }
11
+ export interface DialogInfo {
12
+ title: string;
13
+ body: string;
14
+ action: "accept" | "deny";
15
+ }
16
+ export interface InterceptResult {
17
+ handled: boolean;
18
+ dialog?: DialogInfo;
19
+ }
20
+ export declare class PermissionInterceptor {
21
+ private readonly screen;
22
+ private mode;
23
+ private readonly onDialog?;
24
+ constructor(opts: PermissionInterceptorOpts);
25
+ getMode(): InterceptorMode;
26
+ setMode(mode: InterceptorMode): void;
27
+ checkAndHandle(): Promise<InterceptResult>;
28
+ }
29
+ export declare function modeFromEnv(value: string | undefined): InterceptorMode;
@@ -0,0 +1,185 @@
1
+ const KNOWN_DIALOGS = [
2
+ {
3
+ id: "camera",
4
+ titlePattern: /Would Like to Access the Camera/i,
5
+ acceptButtons: ["OK", "Allow"],
6
+ },
7
+ {
8
+ id: "microphone",
9
+ titlePattern: /Would Like to Access the Microphone/i,
10
+ acceptButtons: ["OK", "Allow"],
11
+ },
12
+ {
13
+ id: "photos",
14
+ titlePattern: /Would Like to Access Your Photos/i,
15
+ acceptButtons: [
16
+ "Allow Access to All Photos",
17
+ "Allow Full Access",
18
+ "Allow",
19
+ ],
20
+ },
21
+ {
22
+ id: "location",
23
+ titlePattern: /Allow .* to Use Your Location\??/i,
24
+ acceptButtons: [
25
+ "Allow While Using App",
26
+ "Allow Once",
27
+ "Allow",
28
+ ],
29
+ },
30
+ {
31
+ id: "notifications",
32
+ titlePattern: /Would Like to Send You Notifications/i,
33
+ acceptButtons: ["Allow"],
34
+ },
35
+ {
36
+ id: "local-network",
37
+ titlePattern: /Would Like to Find and Connect to Devices on Your Local Network/i,
38
+ acceptButtons: ["Allow", "OK"],
39
+ },
40
+ {
41
+ id: "bluetooth",
42
+ titlePattern: /Would Like to Use Bluetooth/i,
43
+ acceptButtons: ["OK", "Allow"],
44
+ },
45
+ {
46
+ id: "tracking",
47
+ titlePattern: /Allow .* to track your activity|would like permission to track/i,
48
+ acceptButtons: ["Allow"],
49
+ },
50
+ {
51
+ id: "faceid",
52
+ titlePattern: /Would Like to Use Face ID/i,
53
+ acceptButtons: ["OK", "Allow"],
54
+ },
55
+ ];
56
+ const ALERT_TYPE_PATTERNS = [
57
+ /XCUIElementTypeAlert/i,
58
+ /SBAlertController/i,
59
+ ];
60
+ function isAlertElement(el) {
61
+ const type = String(el.type ?? "");
62
+ return ALERT_TYPE_PATTERNS.some((p) => p.test(type));
63
+ }
64
+ function findAlert(root) {
65
+ if (isAlertElement(root))
66
+ return root;
67
+ const children = root.children;
68
+ if (Array.isArray(children)) {
69
+ for (const child of children) {
70
+ const found = findAlert(child);
71
+ if (found)
72
+ return found;
73
+ }
74
+ }
75
+ return null;
76
+ }
77
+ function collectAlertContents(root) {
78
+ const texts = [];
79
+ const buttons = [];
80
+ const visit = (el) => {
81
+ const type = String(el.type ?? "");
82
+ const label = String(el.label ?? el.name ?? "");
83
+ const value = String(el.value ?? "");
84
+ if (/XCUIElementTypeStaticText/i.test(type)) {
85
+ const text = label || value;
86
+ if (text)
87
+ texts.push(text);
88
+ }
89
+ else if (/XCUIElementTypeButton/i.test(type)) {
90
+ if (label)
91
+ buttons.push({ label, element: el });
92
+ }
93
+ else if (!type) {
94
+ }
95
+ const children = el.children;
96
+ if (Array.isArray(children)) {
97
+ for (const child of children)
98
+ visit(child);
99
+ }
100
+ };
101
+ visit(root);
102
+ return { texts, buttons };
103
+ }
104
+ export class PermissionInterceptor {
105
+ screen;
106
+ mode;
107
+ onDialog;
108
+ constructor(opts) {
109
+ this.screen = opts.screen;
110
+ this.mode = opts.mode;
111
+ this.onDialog = opts.onDialog;
112
+ }
113
+ getMode() {
114
+ return this.mode;
115
+ }
116
+ setMode(mode) {
117
+ this.mode = mode;
118
+ }
119
+ async checkAndHandle() {
120
+ if (this.mode === "off") {
121
+ return { handled: false };
122
+ }
123
+ const src = await this.screen.getSource().catch(() => undefined);
124
+ if (!src?.success || !src.source) {
125
+ return { handled: false };
126
+ }
127
+ const alert = findAlert(src.source);
128
+ if (!alert)
129
+ return { handled: false };
130
+ const { texts, buttons } = collectAlertContents(alert);
131
+ if (texts.length === 0 && buttons.length === 0) {
132
+ return { handled: false };
133
+ }
134
+ const title = texts[0] ?? "";
135
+ const body = texts.slice(1).join(" ");
136
+ const pattern = KNOWN_DIALOGS.find((p) => p.titlePattern.test(title));
137
+ if (!pattern) {
138
+ return { handled: false };
139
+ }
140
+ let action = "accept";
141
+ if (this.mode === "ask") {
142
+ if (this.onDialog) {
143
+ try {
144
+ action = await this.onDialog({ title, body });
145
+ }
146
+ catch {
147
+ action = "accept";
148
+ }
149
+ }
150
+ }
151
+ const buttonLabels = action === "accept"
152
+ ? pattern.acceptButtons
153
+ : pattern.denyButtons ?? ["Don't Allow", "Cancel"];
154
+ const target = buttonLabels
155
+ .map((preferred) => buttons.find((b) => b.label.toLowerCase() === preferred.toLowerCase()))
156
+ .find((b) => !!b);
157
+ if (!target) {
158
+ return {
159
+ handled: false,
160
+ dialog: { title, body, action },
161
+ };
162
+ }
163
+ const tap = await this.screen.findAndTap({
164
+ accessibilityLabel: target.label,
165
+ });
166
+ if (!tap.success) {
167
+ return {
168
+ handled: false,
169
+ dialog: { title, body, action },
170
+ };
171
+ }
172
+ return {
173
+ handled: true,
174
+ dialog: { title, body, action },
175
+ };
176
+ }
177
+ }
178
+ export function modeFromEnv(value) {
179
+ const v = (value ?? "").toLowerCase();
180
+ if (v === "off" || v === "false" || v === "0")
181
+ return "off";
182
+ if (v === "ask")
183
+ return "ask";
184
+ return "auto-accept";
185
+ }
@@ -1,8 +1,3 @@
1
- /**
2
- * Detects when a clean Expo prebuild is needed by comparing
3
- * current project state against a cached snapshot from the last
4
- * successful build.
5
- */
6
1
  interface PrebuildCheckResult {
7
2
  needsClean: boolean;
8
3
  reasons: string[];
@@ -12,38 +7,13 @@ export declare class PrebuildDetector {
12
7
  private projectDir;
13
8
  private snapshotPath;
14
9
  constructor(projectDir: string, cacheDir: string);
15
- /**
16
- * Check if a file exists.
17
- */
18
10
  private fileExists;
19
- /**
20
- * Read and parse a JSON file, returning null if it doesn't exist or is invalid.
21
- */
22
11
  private readJson;
23
- /**
24
- * Compute a simple hash of a file's contents for change detection.
25
- * Uses a basic string hash rather than crypto to keep it lightweight.
26
- */
27
12
  private hashFile;
28
- /**
29
- * Read the current project state for snapshot comparison.
30
- */
31
13
  private getCurrentState;
32
- /**
33
- * Load the cached snapshot from the last successful build.
34
- */
35
14
  private loadSnapshot;
36
- /**
37
- * Save a snapshot after a successful build.
38
- */
39
15
  saveSnapshot(): Promise<void>;
40
- /**
41
- * Diff two dependency maps and return lists of added, removed, and changed entries.
42
- */
43
16
  private diffDependencies;
44
- /**
45
- * Check whether a clean prebuild is likely needed.
46
- */
47
17
  check(): Promise<PrebuildCheckResult>;
48
18
  }
49
19
  export {};
@@ -1,8 +1,3 @@
1
- /**
2
- * Detects when a clean Expo prebuild is needed by comparing
3
- * current project state against a cached snapshot from the last
4
- * successful build.
5
- */
6
1
  import { readFile, writeFile, mkdir, access } from "node:fs/promises";
7
2
  import { join } from "node:path";
8
3
  export class PrebuildDetector {
@@ -14,9 +9,6 @@ export class PrebuildDetector {
14
9
  this.cacheDir = cacheDir;
15
10
  this.snapshotPath = join(cacheDir, "last-build-snapshot.json");
16
11
  }
17
- /**
18
- * Check if a file exists.
19
- */
20
12
  async fileExists(path) {
21
13
  try {
22
14
  await access(path);
@@ -26,9 +18,6 @@ export class PrebuildDetector {
26
18
  return false;
27
19
  }
28
20
  }
29
- /**
30
- * Read and parse a JSON file, returning null if it doesn't exist or is invalid.
31
- */
32
21
  async readJson(path) {
33
22
  try {
34
23
  const content = await readFile(path, "utf-8");
@@ -38,10 +27,6 @@ export class PrebuildDetector {
38
27
  return null;
39
28
  }
40
29
  }
41
- /**
42
- * Compute a simple hash of a file's contents for change detection.
43
- * Uses a basic string hash rather than crypto to keep it lightweight.
44
- */
45
30
  async hashFile(path) {
46
31
  try {
47
32
  const content = await readFile(path, "utf-8");
@@ -49,7 +34,7 @@ export class PrebuildDetector {
49
34
  for (let i = 0; i < content.length; i++) {
50
35
  const chr = content.charCodeAt(i);
51
36
  hash = (hash << 5) - hash + chr;
52
- hash |= 0; // Convert to 32-bit integer
37
+ hash |= 0;
53
38
  }
54
39
  return hash.toString(36);
55
40
  }
@@ -57,15 +42,10 @@ export class PrebuildDetector {
57
42
  return "missing";
58
43
  }
59
44
  }
60
- /**
61
- * Read the current project state for snapshot comparison.
62
- */
63
45
  async getCurrentState() {
64
46
  const pkgJson = await this.readJson(join(this.projectDir, "package.json"));
65
- // Try app.json first, fall back to app.config.js/ts content
66
47
  let appConfig = await this.readJson(join(this.projectDir, "app.json"));
67
48
  if (!appConfig) {
68
- // For app.config.js/ts, we just hash the file instead of evaluating it
69
49
  const configHash = await this.hashFile(join(this.projectDir, "app.config.js"));
70
50
  const configTsHash = await this.hashFile(join(this.projectDir, "app.config.ts"));
71
51
  appConfig = {
@@ -82,9 +62,6 @@ export class PrebuildDetector {
82
62
  timestamp: new Date().toISOString(),
83
63
  };
84
64
  }
85
- /**
86
- * Load the cached snapshot from the last successful build.
87
- */
88
65
  async loadSnapshot() {
89
66
  try {
90
67
  const content = await readFile(this.snapshotPath, "utf-8");
@@ -94,18 +71,11 @@ export class PrebuildDetector {
94
71
  return null;
95
72
  }
96
73
  }
97
- /**
98
- * Save a snapshot after a successful build.
99
- */
100
74
  async saveSnapshot() {
101
75
  const state = await this.getCurrentState();
102
- // Ensure cache directory exists
103
76
  await mkdir(this.cacheDir, { recursive: true });
104
77
  await writeFile(this.snapshotPath, JSON.stringify(state, null, 2), "utf-8");
105
78
  }
106
- /**
107
- * Diff two dependency maps and return lists of added, removed, and changed entries.
108
- */
109
79
  diffDependencies(oldDeps, newDeps) {
110
80
  const added = [];
111
81
  const removed = [];
@@ -127,12 +97,8 @@ export class PrebuildDetector {
127
97
  }
128
98
  return { added, removed, changed };
129
99
  }
130
- /**
131
- * Check whether a clean prebuild is likely needed.
132
- */
133
100
  async check() {
134
101
  const reasons = [];
135
- // Check if ios/ directory exists at all
136
102
  const iosExists = await this.fileExists(join(this.projectDir, "ios"));
137
103
  if (!iosExists) {
138
104
  return {
@@ -142,7 +108,6 @@ export class PrebuildDetector {
142
108
  ],
143
109
  };
144
110
  }
145
- // Load the last build snapshot
146
111
  const snapshot = await this.loadSnapshot();
147
112
  if (!snapshot) {
148
113
  return {
@@ -153,9 +118,7 @@ export class PrebuildDetector {
153
118
  ],
154
119
  };
155
120
  }
156
- // Get current state
157
121
  const current = await this.getCurrentState();
158
- // Compare dependencies
159
122
  const depDiff = this.diffDependencies(snapshot.dependencies, current.dependencies);
160
123
  if (depDiff.added.length > 0) {
161
124
  reasons.push(`New dependencies added: ${depDiff.added.join(", ")}. ` +
@@ -166,7 +129,6 @@ export class PrebuildDetector {
166
129
  "Stale native modules may cause build errors.");
167
130
  }
168
131
  if (depDiff.changed.length > 0) {
169
- // Only flag native-related version changes as needing clean prebuild
170
132
  const nativeIndicators = [
171
133
  "react-native",
172
134
  "expo",
@@ -179,7 +141,6 @@ export class PrebuildDetector {
179
141
  "Clean prebuild recommended.");
180
142
  }
181
143
  }
182
- // Compare devDependencies
183
144
  const devDepDiff = this.diffDependencies(snapshot.devDependencies, current.devDependencies);
184
145
  if (devDepDiff.added.length > 0 || devDepDiff.removed.length > 0) {
185
146
  const nativeDevDeps = [
@@ -192,14 +153,12 @@ export class PrebuildDetector {
192
153
  reasons.push(`Native dev dependencies changed: ${nativeDevDeps.join(", ")}.`);
193
154
  }
194
155
  }
195
- // Compare app config
196
156
  const configChanged = JSON.stringify(snapshot.appConfig) !==
197
157
  JSON.stringify(current.appConfig);
198
158
  if (configChanged) {
199
159
  reasons.push("app.json / app.config.js has changed since last build. " +
200
160
  "Expo plugins or native config may have changed.");
201
161
  }
202
- // Compare Podfile.lock hash
203
162
  if (current.podfileLockHash === "missing") {
204
163
  reasons.push("ios/Podfile.lock is missing. Pods may need to be installed.");
205
164
  }