serve-sim 0.1.22 → 0.1.24

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "serve-sim",
3
- "version": "0.1.22",
3
+ "version": "0.1.24",
4
4
  "type": "module",
5
5
  "author": {
6
6
  "name": "Evan Bacon",
@@ -27,11 +27,18 @@
27
27
  "dist/serve-sim.js",
28
28
  "dist/middleware.js",
29
29
  "dist/middleware.cjs",
30
+ "dist/simcam/libSimCameraInjector.dylib",
31
+ "dist/simcam/serve-sim-camera-helper",
30
32
  "src/ax-shared.ts",
31
33
  "src/ax.ts",
32
34
  "src/middleware.ts",
33
35
  "src/state.ts",
34
- "bin/serve-sim-bin"
36
+ "bin/serve-sim-bin",
37
+ "Sources/SimCameraInjector/SimCameraInjector.m",
38
+ "Sources/SimCameraInjector/include/SimCamShared.h",
39
+ "Sources/SimCameraInjector/build.sh",
40
+ "Sources/SimCameraHelper/main.m",
41
+ "Sources/SimCameraHelper/build.sh"
35
42
  ],
36
43
  "exports": {
37
44
  "./middleware": {
@@ -51,10 +58,14 @@
51
58
  },
52
59
  "devDependencies": {
53
60
  "@types/bun": "latest",
54
- "serve-sim-client": "workspace:*",
61
+ "@types/react": "^19.0.0",
62
+ "@types/react-dom": "^19.0.0",
63
+ "bun-plugin-tailwind": "^0.1.2",
55
64
  "preact": "^10.29.1",
56
65
  "react": "^19.0.0",
57
66
  "react-dom": "^19.0.0",
67
+ "serve-sim-client": "workspace:*",
68
+ "tailwindcss": "^4.1.7",
58
69
  "typescript": "^5.7.0"
59
70
  },
60
71
  "dependencies": {
package/src/ax.ts CHANGED
@@ -62,12 +62,12 @@ function normalizeAxTree(roots: RawAxeNode[]): AxSnapshot {
62
62
  }
63
63
 
64
64
  for (let index = 0; index < node.children.length && elements.length < MAX_ELEMENTS; index++) {
65
- visit(node.children[index], `${path}.${index}`);
65
+ visit(node.children[index]!, `${path}.${index}`);
66
66
  }
67
67
  };
68
68
 
69
69
  for (let index = 0; index < roots.length && elements.length < MAX_ELEMENTS; index++) {
70
- visit(roots[index], String(index));
70
+ visit(roots[index]!, String(index));
71
71
  }
72
72
 
73
73
  return {
package/src/middleware.ts CHANGED
@@ -1,10 +1,15 @@
1
1
  import { readdirSync, readFileSync, existsSync, unlinkSync } from "fs";
2
- import { execSync, spawn, exec, execFile, type ChildProcess } from "child_process";
2
+ import { execSync, spawn, exec, execFile, type ChildProcess, type ExecException } from "child_process";
3
3
  import { tmpdir } from "os";
4
4
  import { join } from "path";
5
5
  import { createServer as createNetServer } from "net";
6
+ import type { IncomingMessage, ServerResponse } from "http";
6
7
  import { createAxStreamerCache } from "./ax";
7
8
 
9
+ type SimReq = IncomingMessage;
10
+ type SimRes = ServerResponse;
11
+ type SimNext = (err?: unknown) => void;
12
+
8
13
  // Injected at build time as a base64-encoded string via `define`
9
14
  declare const __PREVIEW_HTML_B64__: string;
10
15
  const STATE_DIR = join(tmpdir(), "serve-sim");
@@ -31,6 +36,41 @@ type WebKitBridge = {
31
36
  releaseHighlight?(targetId?: string): void;
32
37
  };
33
38
 
39
+ type InspectWebKitBridgeTarget = {
40
+ targetId: string;
41
+ title?: string;
42
+ appName?: string;
43
+ url?: string;
44
+ type?: string;
45
+ bundleId?: string;
46
+ inUseByOtherInspector?: boolean;
47
+ source?: { kind?: string; id?: string };
48
+ };
49
+
50
+ type CdpHttpListEntry = {
51
+ id: string;
52
+ title: string;
53
+ url: string;
54
+ type: string;
55
+ description?: string;
56
+ };
57
+
58
+ type CdpHttpVersion = { Browser?: string };
59
+
60
+ type SimctlBootedList = {
61
+ devices: Record<string, Array<{ udid: string; state: string }>>;
62
+ };
63
+
64
+ type SimctlAllList = {
65
+ devices: Record<string, Array<Omit<SimctlDevice, "runtime">>>;
66
+ };
67
+
68
+ type ShutdownRequestBody = { udid?: string };
69
+ type StartRequestBody = { udid?: string };
70
+ type ReleaseRequestBody = { targetId?: string };
71
+ type HighlightRequestBody = { targetId?: string; on?: boolean };
72
+ type ExecRequestBody = { command?: string };
73
+
34
74
  export interface ServeSimState {
35
75
  pid: number;
36
76
  port: number;
@@ -123,53 +163,6 @@ export function matchInstalledAppByDisplayName(
123
163
  return null;
124
164
  }
125
165
 
126
- function shellQuote(value: string): string {
127
- return "'" + value.replace(/'/g, "'\\''") + "'";
128
- }
129
-
130
- function installedAppsForDevice(udid: string): Promise<Record<string, InstalledApp>> {
131
- return new Promise((resolve) => {
132
- exec(
133
- `xcrun simctl listapps ${shellQuote(udid)} | plutil -convert json -o - -`,
134
- { timeout: 3_000 },
135
- (err, stdout) => {
136
- if (err || !stdout) return resolve({});
137
- try {
138
- resolve(JSON.parse(stdout) as Record<string, InstalledApp>);
139
- } catch {
140
- resolve({});
141
- }
142
- },
143
- );
144
- });
145
- }
146
-
147
- async function detectCurrentForegroundApp(
148
- udid: string,
149
- stateUrl: string,
150
- ): Promise<{ bundleId: string; isReactNative: boolean } | null> {
151
- const controller = new AbortController();
152
- const timer = setTimeout(() => controller.abort(), 2_000);
153
- try {
154
- const res = await fetch(`${stateUrl}/ax`, { signal: controller.signal });
155
- if (!res.ok) return null;
156
- const tree = await res.json() as Array<{ AXLabel?: unknown }>;
157
- const displayName = tree?.[0]?.AXLabel;
158
- if (typeof displayName !== "string") return null;
159
-
160
- const bundleId = matchInstalledAppByDisplayName(
161
- await installedAppsForDevice(udid),
162
- displayName,
163
- );
164
- if (!bundleId || !isUserFacingBundle(bundleId)) return null;
165
- return { bundleId, isReactNative: await detectReactNative(udid, bundleId) };
166
- } catch {
167
- return null;
168
- } finally {
169
- clearTimeout(timer);
170
- }
171
- }
172
-
173
166
  // Cache simctl's booted-device set briefly so per-request cost stays bounded.
174
167
  // The middleware runs inside the user's dev server (Metro etc.) and
175
168
  // readServeSimStates() is called on every /api and every page load.
@@ -185,9 +178,7 @@ function getBootedUdids(): Set<string> | null {
185
178
  stdio: ["ignore", "pipe", "pipe"],
186
179
  timeout: 3_000,
187
180
  });
188
- const data = JSON.parse(output) as {
189
- devices: Record<string, Array<{ udid: string; state: string }>>;
190
- };
181
+ const data = JSON.parse(output) as SimctlBootedList;
191
182
  const booted = new Set<string>();
192
183
  for (const runtime of Object.values(data.devices)) {
193
184
  for (const device of runtime) {
@@ -272,7 +263,7 @@ async function existingInspectWebKitBridge(port: number): Promise<WebKitBridge |
272
263
  try {
273
264
  const versionRes = await fetch(`${cdpUrl}/json/version`);
274
265
  if (!versionRes.ok) return null;
275
- const version = await versionRes.json() as { Browser?: string };
266
+ const version = await versionRes.json() as CdpHttpVersion;
276
267
  if (version.Browser !== "Safari/inspect-webkit") return null;
277
268
  return {
278
269
  port,
@@ -283,13 +274,7 @@ async function existingInspectWebKitBridge(port: number): Promise<WebKitBridge |
283
274
  // shape `sim:<udid>:<appId>:<pageId>` and the description string
284
275
  // `<deviceLabel> (<bundleId>)` are all we have here.
285
276
  const listRes = await fetch(`${cdpUrl}/json/list`);
286
- const targets = await listRes.json() as Array<{
287
- id: string;
288
- title: string;
289
- url: string;
290
- type: string;
291
- description?: string;
292
- }>;
277
+ const targets = await listRes.json() as CdpHttpListEntry[];
293
278
  return targets
294
279
  .filter((target) => target.id.startsWith("sim:"))
295
280
  .map((target) => {
@@ -335,29 +320,35 @@ async function ensureInspectWebKitBridge(): Promise<WebKitBridge> {
335
320
  // (and what the DevTools frontend CSP whitelists). `localhost` resolves
336
321
  // to ::1 first on some setups, which would leave the iframe's
337
322
  // ws://127.0.0.1:9222 connection refused.
338
- const server = await startCdpServer({ host: "127.0.0.1", port });
323
+ const server = await startCdpServer({ host: "127.0.0.1", port }) as Awaited<ReturnType<typeof startCdpServer>> & {
324
+ highlightTarget?(targetId: string, on: boolean): Promise<void>;
325
+ releaseHighlight?(targetId?: string): void;
326
+ };
339
327
  return {
340
328
  port,
341
329
  cdpUrl: `http://127.0.0.1:${port}`,
342
330
  async listTargets() {
343
- return server.getTargets()
344
- .filter((target: any) => target.source?.kind === "simulator")
345
- .map((target: any) => ({
346
- id: target.targetId,
347
- title: target.title || target.appName || target.url || "Untitled",
348
- url: /^https?:/i.test(target.url) ? target.url : "about:blank",
349
- type: target.type || "page",
350
- appName: target.appName,
351
- bundleId: target.bundleId,
352
- udid: target.source?.id,
353
- inUseByOtherInspector: !!target.inUseByOtherInspector,
354
- }));
331
+ return (server.getTargets() as InspectWebKitBridgeTarget[])
332
+ .filter((target) => target.source?.kind === "simulator")
333
+ .map((target) => {
334
+ const url = target.url ?? "";
335
+ return {
336
+ id: target.targetId,
337
+ title: target.title || target.appName || url || "Untitled",
338
+ url: /^https?:/i.test(url) ? url : "about:blank",
339
+ type: target.type || "page",
340
+ appName: target.appName,
341
+ bundleId: target.bundleId,
342
+ udid: target.source?.id,
343
+ inUseByOtherInspector: !!target.inUseByOtherInspector,
344
+ };
345
+ });
355
346
  },
356
347
  highlightTarget: server.highlightTarget?.bind(server),
357
348
  releaseHighlight: server.releaseHighlight?.bind(server),
358
349
  };
359
- } catch (err: any) {
360
- if (err?.code === "EADDRINUSE") {
350
+ } catch (err) {
351
+ if ((err as NodeJS.ErrnoException)?.code === "EADDRINUSE") {
361
352
  const existing = await existingInspectWebKitBridge(port);
362
353
  if (existing) return existing;
363
354
  continue;
@@ -392,6 +383,19 @@ function bridgeWsHost(_reqHost: string | undefined, bridgePort: number): string
392
383
  }
393
384
 
394
385
  let _html: string | null = null;
386
+ /**
387
+ * Best-effort absolute path to the running serve-sim entry script. Used so
388
+ * the in-page Camera tool can `node <path> camera ...` regardless of PATH.
389
+ * Falls back to the literal `serve-sim` if we can't determine a usable path.
390
+ */
391
+ function serveSimBinPath(): string {
392
+ try {
393
+ const argv = process.argv;
394
+ if (argv[1] && existsSync(argv[1])) return argv[1];
395
+ } catch {}
396
+ return "serve-sim";
397
+ }
398
+
395
399
  function loadHtml(): string {
396
400
  if (!_html) {
397
401
  _html = Buffer.from(__PREVIEW_HTML_B64__, "base64").toString("utf-8");
@@ -414,9 +418,7 @@ function listAllSimulators(): SimctlDevice[] {
414
418
  stdio: ["ignore", "pipe", "ignore"],
415
419
  timeout: 3_000,
416
420
  });
417
- const data = JSON.parse(output) as {
418
- devices: Record<string, Array<Omit<SimctlDevice, "runtime">>>;
419
- };
421
+ const data = JSON.parse(output) as SimctlAllList;
420
422
  const out: SimctlDevice[] = [];
421
423
  for (const [runtime, devices] of Object.entries(data.devices)) {
422
424
  // Only iOS (skip watchOS / tvOS / visionOS for the grid MVP — the helper
@@ -433,10 +435,6 @@ function listAllSimulators(): SimctlDevice[] {
433
435
  }
434
436
  }
435
437
 
436
- function deviceNameFor(udid: string): string | null {
437
- return listAllSimulators().find((d) => d.udid === udid)?.name ?? null;
438
- }
439
-
440
438
  // Default per-simulator footprint when we have no running sim to measure
441
439
  // from — a fresh booted iOS sim with one app launched typically sits in
442
440
  // the 1.2–1.8 GB range. Used as a fallback only.
@@ -513,7 +511,7 @@ function readSimulatorMemoryUsage(): { perUdid: Record<string, number>; totalByt
513
511
  const rssKb = Number(line.split(/\s+/, 1)[0]);
514
512
  if (!Number.isFinite(rssKb)) continue;
515
513
  const bytes = rssKb * 1024;
516
- const udid = m[1].toUpperCase();
514
+ const udid = m[1]!.toUpperCase();
517
515
  perUdid[udid] = (perUdid[udid] ?? 0) + bytes;
518
516
  totalBytes += bytes;
519
517
  }
@@ -598,7 +596,7 @@ export interface SimMiddlewareOptions {
598
596
  export function simMiddleware(options?: SimMiddlewareOptions) {
599
597
  const base = (options?.basePath ?? "/.sim").replace(/\/+$/, "");
600
598
 
601
- return (req: any, res: any, next?: () => void) => {
599
+ return (req: SimReq, res: SimRes, next?: SimNext) => {
602
600
  const rawUrl: string = req.url ?? "";
603
601
  const qIndex = rawUrl.indexOf("?");
604
602
  const url = qIndex === -1 ? rawUrl : rawUrl.slice(0, qIndex);
@@ -657,6 +655,10 @@ export function simMiddleware(options?: SimMiddlewareOptions) {
657
655
  appStateEndpoint: endpoint(base, "/appstate", state.device),
658
656
  axEndpoint: endpoint(base, "/ax", state.device),
659
657
  devtoolsEndpoint: endpoint(base, "/devtools", state.device),
658
+ // Forward the absolute path of the running serve-sim entry script
659
+ // so the in-page Camera tool can shell out via `node <bin> camera`
660
+ // without depending on `serve-sim` being on PATH.
661
+ serveSimBin: serveSimBinPath(),
660
662
  gridApiEndpoint: gridApiBase,
661
663
  gridStartEndpoint: gridApiBase + "/start",
662
664
  gridShutdownEndpoint: gridApiBase + "/shutdown",
@@ -743,7 +745,7 @@ export function simMiddleware(options?: SimMiddlewareOptions) {
743
745
  });
744
746
  req.on("end", () => {
745
747
  let udid = "";
746
- try { udid = JSON.parse(body).udid ?? ""; } catch {}
748
+ try { udid = (JSON.parse(body) as ShutdownRequestBody).udid ?? ""; } catch {}
747
749
  if (!/^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$/i.test(udid)) {
748
750
  res.writeHead(400, { "Content-Type": "application/json" });
749
751
  res.end(JSON.stringify({ ok: false, error: "Invalid or missing udid" }));
@@ -776,7 +778,7 @@ export function simMiddleware(options?: SimMiddlewareOptions) {
776
778
  });
777
779
  req.on("end", () => {
778
780
  let udid = "";
779
- try { udid = JSON.parse(body).udid ?? ""; } catch {}
781
+ try { udid = (JSON.parse(body) as StartRequestBody).udid ?? ""; } catch {}
780
782
  if (!/^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$/i.test(udid)) {
781
783
  res.writeHead(400, { "Content-Type": "application/json" });
782
784
  res.end(JSON.stringify({ ok: false, error: "Invalid or missing udid" }));
@@ -875,10 +877,10 @@ export function simMiddleware(options?: SimMiddlewareOptions) {
875
877
  // Optional body { targetId } releases just one; empty body releases all.
876
878
  if (url === base + "/devtools/release" && req.method === "POST") {
877
879
  let body = "";
878
- req.on("data", (chunk) => (body += chunk));
880
+ req.on("data", (chunk: Buffer) => (body += chunk));
879
881
  req.on("end", async () => {
880
882
  try {
881
- const parsed = body ? JSON.parse(body) as { targetId?: string } : {};
883
+ const parsed: ReleaseRequestBody = body ? JSON.parse(body) : {};
882
884
  const bridge = await ensureInspectWebKitBridge();
883
885
  bridge.releaseHighlight?.(parsed.targetId);
884
886
  res.writeHead(200, { "Content-Type": "application/json" });
@@ -898,10 +900,10 @@ export function simMiddleware(options?: SimMiddlewareOptions) {
898
900
  // { targetId: string, on: boolean }.
899
901
  if (url === base + "/devtools/highlight" && req.method === "POST") {
900
902
  let body = "";
901
- req.on("data", (chunk) => (body += chunk));
903
+ req.on("data", (chunk: Buffer) => (body += chunk));
902
904
  req.on("end", async () => {
903
905
  try {
904
- const { targetId, on } = JSON.parse(body || "{}") as { targetId?: string; on?: boolean };
906
+ const { targetId, on } = JSON.parse(body || "{}") as HighlightRequestBody;
905
907
  if (!targetId) {
906
908
  res.writeHead(400, { "Content-Type": "application/json" });
907
909
  res.end(JSON.stringify({ error: "Missing targetId" }));
@@ -971,7 +973,7 @@ export function simMiddleware(options?: SimMiddlewareOptions) {
971
973
  req.on("end", () => {
972
974
  let command = "";
973
975
  try {
974
- command = JSON.parse(body).command ?? "";
976
+ command = (JSON.parse(body) as ExecRequestBody).command ?? "";
975
977
  } catch {}
976
978
  if (!command) {
977
979
  res.writeHead(400, { "Content-Type": "application/json" });
@@ -983,7 +985,7 @@ export function simMiddleware(options?: SimMiddlewareOptions) {
983
985
  res.end(JSON.stringify({
984
986
  stdout: stdout.toString(),
985
987
  stderr: stderr.toString(),
986
- exitCode: err ? (err as any).code ?? 1 : 0,
988
+ exitCode: err ? (err as ExecException).code ?? 1 : 0,
987
989
  }));
988
990
  });
989
991
  });