Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
732 changes: 390 additions & 342 deletions packages/serve-sim-client/src/simulator/SimulatorView.tsx

Large diffs are not rendered by default.

39 changes: 32 additions & 7 deletions packages/serve-sim/Sources/SimStreamHelper/HTTPServer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -70,17 +70,42 @@ final class HTTPServer {
}
)

// Config endpoint
server["/config"] = { [weak self] request in
let size = self?.clientManager ?? nil
let w = size?.screenWidth ?? 0
let h = size?.screenHeight ?? 0
return HttpResponse.ok(.json(["width": w, "height": h] as AnyObject))
// Config endpoint (must send CORS — preview UI is often http://localhost:* while helper is http://127.0.0.1:*)
server["/config"] = { [weak self] _ in
let w = self?.clientManager.screenWidth ?? 0
let h = self?.clientManager.screenHeight ?? 0
let obj: [String: Int] = ["width": w, "height": h]
guard let jsonData = try? JSONSerialization.data(withJSONObject: obj) else {
return HttpResponse.raw(500, "Internal Server Error", [
"Access-Control-Allow-Origin": "*",
]) { _ in }
}
let bytes = [UInt8](jsonData)
return HttpResponse.raw(200, "OK", [
"Content-Type": "application/json",
"Cache-Control": "no-store",
"Access-Control-Allow-Origin": "*",
]) { writer in
try writer.write(bytes)
}
}

// Health endpoint
server["/health"] = { _ in
return .ok(.json(["status": "ok"] as AnyObject))
let obj: [String: String] = ["status": "ok"]
guard let jsonData = try? JSONSerialization.data(withJSONObject: obj) else {
return HttpResponse.raw(500, "Internal Server Error", [
"Access-Control-Allow-Origin": "*",
]) { _ in }
}
let bytes = [UInt8](jsonData)
return HttpResponse.raw(200, "OK", [
"Content-Type": "application/json",
"Cache-Control": "no-store",
"Access-Control-Allow-Origin": "*",
]) { writer in
try writer.write(bytes)
}
}

// CORS preflight
Expand Down
Binary file modified packages/serve-sim/bin/serve-sim-bin
Binary file not shown.
100 changes: 82 additions & 18 deletions packages/serve-sim/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,27 @@
* via the __PREVIEW_HTML_B64__ build-time define.
*/
import { resolve, dirname } from "path";
import { mkdirSync, writeFileSync, rmSync, readFileSync } from "fs";
import { mkdirSync, writeFileSync, rmSync, readFileSync, existsSync } from "fs";
import { spawnSync } from "child_process";

const root = import.meta.dir;
const distDir = resolve(root, "dist");

/** Resolve a hoisted workspace dependency (preact may live in repo root `node_modules`). */
function resolvePackageDir(packageName: string, startDir: string): string {
let dir = startDir;
for (;;) {
const candidate = resolve(dir, "node_modules", packageName);
if (existsSync(resolve(candidate, "package.json"))) return candidate;
const parent = dirname(dir);
if (parent === dir) {
throw new Error(
`Could not find "${packageName}" under node_modules (walked up from ${startDir}). Run install from the repo root.`,
);
}
dir = parent;
}
}
rmSync(distDir, { recursive: true, force: true });
mkdirSync(distDir, { recursive: true });

Expand All @@ -27,23 +43,36 @@ function kb(n: number): string {

// ─── 1. Bundle the browser client (React aliased to Preact) ───────────────

const preactRoot = resolvePackageDir("preact", root);
const preactCompat = resolve(preactRoot, "compat/dist/compat.module.js");
const preactCompatClient = resolve(preactRoot, "compat/client.mjs");
const preactJsxRuntime = resolve(
preactRoot,
"jsx-runtime/dist/jsxRuntime.module.js",
);

const clientResult = await Bun.build({
entrypoints: [resolve(root, "src/client/client.tsx")],
minify: true,
target: "browser",
format: "esm",
define: { "process.env.NODE_ENV": '"production"' },
plugins: [{
name: "preact-alias",
setup(build) {
const preactCompat = resolve(root, "node_modules/preact/compat/dist/compat.module.js");
const preactCompatClient = resolve(root, "node_modules/preact/compat/client.mjs");
const preactJsxRuntime = resolve(root, "node_modules/preact/jsx-runtime/dist/jsxRuntime.module.js");
build.onResolve({ filter: /^react-dom\/client$/ }, () => ({ path: preactCompatClient }));
build.onResolve({ filter: /^react(-dom)?$/ }, () => ({ path: preactCompat }));
build.onResolve({ filter: /^react\/jsx(-dev)?-runtime$/ }, () => ({ path: preactJsxRuntime }));
plugins: [
{
name: "preact-alias",
setup(build) {
build.onResolve({ filter: /^react-dom\/client$/ }, () => ({
path: preactCompatClient,
}));
build.onResolve({ filter: /^react(-dom)?$/ }, () => ({
path: preactCompat,
}));
build.onResolve({ filter: /^react\/jsx(-dev)?-runtime$/ }, () => ({
path: preactJsxRuntime,
}));
},
},
}],
],
});

if (!clientResult.success) {
Expand All @@ -52,14 +81,19 @@ if (!clientResult.success) {
process.exit(1);
}

const clientJs = (await clientResult.outputs[0].text()).replace(/<\/script>/gi, "<\\/script>");
const clientJs = (await clientResult.outputs[0].text()).replace(
/<\/script>/gi,
"<\\/script>",
);
console.log(`client bundle ${kb(clientJs.length)}`);

// ─── 2. Inline client into preview HTML, base64-encode for the define ────

// Committed ICO copy of Simulator.app's AppIcon, inlined as a data URI so the
// preview tab shows the same icon as the native app.
const faviconBytes = readFileSync(resolve(root, "src/client/simulator-icon.ico"));
const faviconBytes = readFileSync(
resolve(root, "src/client/simulator-icon.ico"),
);
const faviconTag = `<link rel="icon" type="image/x-icon" href="data:image/x-icon;base64,${faviconBytes.toString("base64")}">`;
console.log(`favicon ${kb(faviconBytes.length)}`);

Expand All @@ -77,7 +111,9 @@ ${faviconTag}
</body></html>`;

const htmlB64 = Buffer.from(html).toString("base64");
console.log(`preview html ${kb(html.length)} (base64 ${kb(htmlB64.length)})`);
console.log(
`preview html ${kb(html.length)} (base64 ${kb(htmlB64.length)})`,
);

const PREVIEW_DEFINE = { __PREVIEW_HTML_B64__: JSON.stringify(htmlB64) };

Expand All @@ -89,7 +125,17 @@ const mwResult = await Bun.build({
format: "esm",
minify: true,
outdir: distDir,
external: ["fs", "path", "os", "child_process", "url"],
external: [
"fs",
"path",
"os",
"child_process",
"url",
"http",
"http-proxy",
"net",
"stream",
],
define: PREVIEW_DEFINE,
});
if (!mwResult.success) {
Expand All @@ -115,7 +161,23 @@ const binJsResult = await Bun.build({
minify: true,
outdir: distDir,
naming: "serve-sim.js",
external: ["fs", "path", "os", "child_process", "url", "net", "tls", "crypto", "stream", "events", "http", "https", "zlib", "buffer"],
external: [
"fs",
"path",
"os",
"child_process",
"url",
"net",
"tls",
"crypto",
"stream",
"events",
"http",
"https",
"http-proxy",
"zlib",
"buffer",
],
define: PREVIEW_DEFINE,
});
if (!binJsResult.success) {
Expand All @@ -137,8 +199,10 @@ const compile = spawnSync(
"--compile",
"--minify",
resolve(root, "src/index.ts"),
"--outfile", resolve(distDir, "serve-sim"),
"--define", `__PREVIEW_HTML_B64__=${JSON.stringify(htmlB64)}`,
"--outfile",
resolve(distDir, "serve-sim"),
"--define",
`__PREVIEW_HTML_B64__=${JSON.stringify(htmlB64)}`,
],
{ stdio: "inherit" },
);
Expand Down
Loading