rastack 0.0.57 → 0.0.58

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.
@@ -202,13 +202,30 @@ function renderShell(title = "React API Stack — admin") {
202
202
  <meta name="viewport" content="width=device-width, initial-scale=1" />
203
203
  <title>${title}</title>
204
204
  <style>
205
- html, body { margin: 0; height: 100%; }
205
+ html, body { margin: 0; height: 100%; background: #F2F5FB;
206
+ font-family: "Inter", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; }
207
+ @media (prefers-color-scheme: dark) { html, body { background: #06080F; } }
206
208
  #root { min-height: 100%; }
207
- .rastack-boot { font: 14px system-ui, sans-serif; padding: 2rem; color: #555; }
209
+ .rastack-boot { min-height: 100vh; display: flex; flex-direction: column;
210
+ align-items: center; justify-content: center; gap: 20px; }
211
+ .rastack-boot-logo { width: 60px; height: 60px; border-radius: 18px; background: #4F6BFF;
212
+ box-shadow: 0 8px 24px rgba(79,107,255,0.30); animation: rastack-boot-bob 3s ease-in-out infinite; }
213
+ .rastack-boot-track { position: relative; width: 168px; height: 4px; border-radius: 999px;
214
+ overflow: hidden; background: rgba(11,16,32,0.10); }
215
+ @media (prefers-color-scheme: dark) { .rastack-boot-track { background: rgba(233,239,255,0.14); } }
216
+ .rastack-boot-track::after { content: ""; position: absolute; top: 0; left: -42%; height: 100%;
217
+ width: 40%; border-radius: inherit; background: linear-gradient(90deg, transparent, #4F6BFF);
218
+ animation: rastack-boot-slide 1.25s cubic-bezier(0.5,0.1,0.3,1) infinite; }
219
+ @keyframes rastack-boot-bob { 0%,100% { transform: translateY(0); } 50% { transform: translateY(-3px); } }
220
+ @keyframes rastack-boot-slide { to { left: 100%; } }
221
+ @media (prefers-reduced-motion: reduce) {
222
+ .rastack-boot-logo { animation: none; }
223
+ .rastack-boot-track::after { animation: none; left: 0; width: 100%; }
224
+ }
208
225
  </style>
209
226
  </head>
210
227
  <body>
211
- <div id="root"><div class="rastack-boot">Loading the WASM engine…</div></div>
228
+ <div id="root"><div class="rastack-boot"><div class="rastack-boot-logo"></div><div class="rastack-boot-track"></div></div></div>
212
229
  <script src="/dev/admin.js"></script>
213
230
  </body>
214
231
  </html>
@@ -57,6 +57,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
57
57
  const child_process_1 = require("child_process");
58
58
  const fs = __importStar(require("fs"));
59
59
  const http = __importStar(require("http"));
60
+ const net = __importStar(require("net"));
60
61
  const path = __importStar(require("path"));
61
62
  const compile_1 = require("./compile");
62
63
  const harness_1 = require("./dev/harness");
@@ -144,14 +145,198 @@ function openBrowser(url) {
144
145
  /* non-fatal — the URL is printed anyway */
145
146
  }
146
147
  }
147
- function main(argv) {
148
+ // ---------------------------------------------------------------------------
149
+ // Live-dev mode — `rastack dev` for an Expo / react-native-web app.
150
+ //
151
+ // A React Native (Expo) app has no static `index.html`: its web build is a
152
+ // Metro bundle. Rather than make the developer run a second tool, `dev` starts
153
+ // Expo's Metro dev server itself and reverse-proxies to it, so `rastack dev`
154
+ // stays the single command *and* keeps Metro's fast-refresh. The WASM
155
+ // static-serve path remains the default for apps that ship a built index.html.
156
+ // ---------------------------------------------------------------------------
157
+ /** True when the project is an Expo app (so `dev` should drive Metro). */
158
+ function isExpoProject(cwd) {
159
+ try {
160
+ const appJson = JSON.parse(fs.readFileSync(path.join(cwd, "app.json"), "utf-8"));
161
+ if (appJson && appJson.expo)
162
+ return true;
163
+ }
164
+ catch {
165
+ /* no app.json — fall through to package.json */
166
+ }
167
+ try {
168
+ const pkg = JSON.parse(fs.readFileSync(path.join(cwd, "package.json"), "utf-8"));
169
+ if (pkg?.dependencies?.expo || pkg?.devDependencies?.expo)
170
+ return true;
171
+ }
172
+ catch {
173
+ /* no package.json */
174
+ }
175
+ return false;
176
+ }
177
+ /** Locate the prebuilt wasm bundle without the cargo fallback (optional here). */
178
+ function findWasmDir() {
179
+ for (const dir of (0, harness_1.wasmBundleCandidates)(HERE)) {
180
+ if (fs.existsSync(path.join(dir, harness_1.WASM_ENTRY)))
181
+ return dir;
182
+ }
183
+ return null;
184
+ }
185
+ /** Grab an ephemeral free TCP port for Metro (avoids a fixed-port clash). */
186
+ function getFreePort() {
187
+ return new Promise((resolve, reject) => {
188
+ const srv = net.createServer();
189
+ srv.on("error", reject);
190
+ srv.listen(0, "127.0.0.1", () => {
191
+ const addr = srv.address();
192
+ const port = addr && typeof addr === "object" ? addr.port : 0;
193
+ srv.close(() => (port ? resolve(port) : reject(new Error("no free port"))));
194
+ });
195
+ });
196
+ }
197
+ /** Start `expo start --web` on `port`, with a reload-enabled environment. */
198
+ function startMetro(cwd, port) {
199
+ // A live dev server needs watch mode, so we must NOT set CI (Expo reads it as
200
+ // "reloads disabled"). BROWSER=none stops Expo opening its own tab — we front
201
+ // it on the rastack port. `shell:true` keeps the npx / npx.cmd split portable.
202
+ const env = {
203
+ ...process.env,
204
+ BROWSER: "none",
205
+ EXPO_NO_TELEMETRY: "1",
206
+ };
207
+ delete env.CI;
208
+ // nosemgrep: javascript.lang.security.detect-child-process.detect-child-process
209
+ const child = (0, child_process_1.spawn)(`npx expo start --web --port ${port}`, {
210
+ cwd,
211
+ env,
212
+ stdio: ["ignore", "pipe", "pipe"],
213
+ shell: true,
214
+ });
215
+ // Surface Metro's own errors (bundling failures, port issues) with a prefix.
216
+ child.stderr?.on("data", (d) => process.stderr.write(` [metro] ${d}`));
217
+ return child;
218
+ }
219
+ /** Poll Metro's `/status` until it answers `packager-status:running`. */
220
+ function waitForMetro(port, timeoutMs = 120000) {
221
+ const deadline = Date.now() + timeoutMs;
222
+ return new Promise((resolve) => {
223
+ const retry = () => Date.now() > deadline ? resolve(false) : setTimeout(tick, 400);
224
+ const tick = () => {
225
+ const req = http.get({ host: "127.0.0.1", port, path: "/status", timeout: 2000 }, (res) => {
226
+ let body = "";
227
+ res.on("data", (c) => (body += c));
228
+ res.on("end", () => body.includes("packager-status:running") ? resolve(true) : retry());
229
+ });
230
+ req.on("error", retry);
231
+ req.on("timeout", () => req.destroy());
232
+ };
233
+ tick();
234
+ });
235
+ }
236
+ /** Reverse-proxy one HTTP request/response pair to Metro. */
237
+ function proxyToMetro(req, res, port) {
238
+ const proxyReq = http.request({ host: "127.0.0.1", port, method: req.method, path: req.url, headers: req.headers }, (proxyRes) => {
239
+ res.writeHead(proxyRes.statusCode || 502, proxyRes.headers);
240
+ proxyRes.pipe(res);
241
+ });
242
+ proxyReq.on("error", () => {
243
+ if (!res.headersSent)
244
+ res.writeHead(502);
245
+ res.end("Metro is not reachable.");
246
+ });
247
+ req.pipe(proxyReq);
248
+ }
249
+ /** Reverse-proxy a WebSocket upgrade (Metro HMR / fast-refresh) to Metro. */
250
+ function proxyUpgrade(req, clientSocket, head, port) {
251
+ const upstream = net.connect(port, "127.0.0.1", () => {
252
+ const lines = [`${req.method} ${req.url} HTTP/1.1`];
253
+ for (const [k, v] of Object.entries(req.headers)) {
254
+ if (Array.isArray(v))
255
+ v.forEach((vv) => lines.push(`${k}: ${vv}`));
256
+ else if (v != null)
257
+ lines.push(`${k}: ${v}`);
258
+ }
259
+ upstream.write(lines.join("\r\n") + "\r\n\r\n");
260
+ if (head && head.length)
261
+ upstream.write(head);
262
+ upstream.pipe(clientSocket);
263
+ clientSocket.pipe(upstream);
264
+ });
265
+ upstream.on("error", () => clientSocket.destroy());
266
+ clientSocket.on("error", () => upstream.destroy());
267
+ }
268
+ /** `rastack dev` for an Expo app: start Metro and reverse-proxy to it. */
269
+ async function runMetroMode(args, cwd) {
270
+ // The wasm engine is optional here — an Expo/RN-web app runs its own engine —
271
+ // so a missing bundle is fine. When present, the reserved backend routes work.
272
+ const wasmDir = findWasmDir();
273
+ const metroPort = await getFreePort();
274
+ console.log(`▸ Starting Metro bundler (Expo) on :${metroPort} …`);
275
+ const metro = startMetro(cwd, metroPort);
276
+ let shuttingDown = false;
277
+ const stop = (code = 0) => {
278
+ if (shuttingDown)
279
+ return;
280
+ shuttingDown = true;
281
+ try {
282
+ metro.kill();
283
+ }
284
+ catch {
285
+ /* already gone */
286
+ }
287
+ process.exit(code);
288
+ };
289
+ process.on("SIGINT", () => stop(0));
290
+ process.on("SIGTERM", () => stop(0));
291
+ metro.on("exit", (code) => {
292
+ if (!shuttingDown) {
293
+ console.error(`\n ✗ Metro exited (code ${code ?? "?"}).`);
294
+ process.exit(code ?? 1);
295
+ }
296
+ });
297
+ if (!(await waitForMetro(metroPort))) {
298
+ console.error(" ✗ Metro did not come up in time.");
299
+ return stop(1);
300
+ }
301
+ const server = http.createServer((req, res) => {
302
+ const url = (req.url || "/").split("?")[0];
303
+ // Reserved backend routes win when the engine bundle is available.
304
+ if (wasmDir && url.startsWith("/wasm/"))
305
+ return serveUnder(res, wasmDir, url.slice("/wasm/".length));
306
+ if (url === "/schema.rastack.json" || url === "/openapi.json")
307
+ return serveUnder(res, args.outDir, path.basename(url));
308
+ if (url.startsWith("/data/warehouse/"))
309
+ return serveUnder(res, args.warehouseDir, url.slice("/data/warehouse/".length));
310
+ // Everything else → the live Metro bundle (HTML, JS, assets, HMR polls).
311
+ return proxyToMetro(req, res, metroPort);
312
+ });
313
+ server.on("upgrade", (req, socket, head) => proxyUpgrade(req, socket, head, metroPort));
314
+ server.listen(args.port, () => {
315
+ const url = `http://localhost:${args.port}`;
316
+ console.log(` ✓ Metro live on :${metroPort} — fast refresh on`);
317
+ if (wasmDir)
318
+ console.log(` ✓ WASM engine: ${path.relative(cwd, wasmDir)}`);
319
+ console.log(`\n ▸ ${url}\n`);
320
+ console.log(" Edits reload live. Need the database console? Run `rastack admin`. Ctrl+C to stop.\n");
321
+ if (args.open)
322
+ openBrowser(url);
323
+ });
324
+ }
325
+ async function main(argv) {
148
326
  const args = (0, harness_1.parseDevArgs)(argv);
327
+ const cwd = process.cwd();
149
328
  console.log(`\n rastack dev — your app, in your browser over WASM\n`);
150
329
  console.log(`▸ Compiling ${args.resourcesDir} → ${args.outDir}`);
151
330
  if (!(0, compile_1.runCompile)(args.resourcesDir, args.outDir)) {
152
331
  console.error(" ✗ compile failed — fix the resource graph and re-run.");
153
332
  process.exit(1);
154
333
  }
334
+ // Serve the developer's own app when there is one; otherwise the welcome page.
335
+ const appRoot = resolveAppRoot(args.appDir);
336
+ // No static build, but an Expo source tree → drive Metro live (fast refresh).
337
+ if (!appRoot && isExpoProject(cwd)) {
338
+ return runMetroMode(args, cwd);
339
+ }
155
340
  const wasmDir = resolveWasmDir();
156
341
  if (!wasmDir) {
157
342
  console.error("\n ✗ No WASM engine available.\n" +
@@ -161,8 +346,6 @@ function main(argv) {
161
346
  " cargo install wasm-bindgen-cli\n");
162
347
  process.exit(1);
163
348
  }
164
- // Serve the developer's own app when there is one; otherwise the welcome page.
165
- const appRoot = resolveAppRoot(args.appDir);
166
349
  const server = http.createServer((req, res) => {
167
350
  const url = (req.url || "/").split("?")[0];
168
351
  if (req.method !== "GET") {
@@ -213,5 +396,8 @@ function main(argv) {
213
396
  });
214
397
  }
215
398
  if (require.main === module) {
216
- main(process.argv.slice(2));
399
+ main(process.argv.slice(2)).catch((err) => {
400
+ console.error(err);
401
+ process.exit(1);
402
+ });
217
403
  }
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rastack",
3
- "version": "0.0.57",
3
+ "version": "0.0.58",
4
4
  "description": "",
5
5
  "main": "runtime.ts",
6
6
  "types": "runtime.ts",
@@ -14,6 +14,7 @@
14
14
  "scripts": {
15
15
  "format": "prettier --write \"**/*.{ts,tsx,md}\"",
16
16
  "build": "tsc && cp src/.prettierrc dist/.prettierrc && node scripts/copy-templates.js && node scripts/build-admin.js && node scripts/copy-wasm.js",
17
+ "dev:install": "node scripts/dev-install.mjs",
17
18
  "start": "node dist/index.js",
18
19
  "test": "jest"
19
20
  },
@@ -191,13 +191,30 @@ export function renderShell(title = "React API Stack — admin"): string {
191
191
  <meta name="viewport" content="width=device-width, initial-scale=1" />
192
192
  <title>${title}</title>
193
193
  <style>
194
- html, body { margin: 0; height: 100%; }
194
+ html, body { margin: 0; height: 100%; background: #F2F5FB;
195
+ font-family: "Inter", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; }
196
+ @media (prefers-color-scheme: dark) { html, body { background: #06080F; } }
195
197
  #root { min-height: 100%; }
196
- .rastack-boot { font: 14px system-ui, sans-serif; padding: 2rem; color: #555; }
198
+ .rastack-boot { min-height: 100vh; display: flex; flex-direction: column;
199
+ align-items: center; justify-content: center; gap: 20px; }
200
+ .rastack-boot-logo { width: 60px; height: 60px; border-radius: 18px; background: #4F6BFF;
201
+ box-shadow: 0 8px 24px rgba(79,107,255,0.30); animation: rastack-boot-bob 3s ease-in-out infinite; }
202
+ .rastack-boot-track { position: relative; width: 168px; height: 4px; border-radius: 999px;
203
+ overflow: hidden; background: rgba(11,16,32,0.10); }
204
+ @media (prefers-color-scheme: dark) { .rastack-boot-track { background: rgba(233,239,255,0.14); } }
205
+ .rastack-boot-track::after { content: ""; position: absolute; top: 0; left: -42%; height: 100%;
206
+ width: 40%; border-radius: inherit; background: linear-gradient(90deg, transparent, #4F6BFF);
207
+ animation: rastack-boot-slide 1.25s cubic-bezier(0.5,0.1,0.3,1) infinite; }
208
+ @keyframes rastack-boot-bob { 0%,100% { transform: translateY(0); } 50% { transform: translateY(-3px); } }
209
+ @keyframes rastack-boot-slide { to { left: 100%; } }
210
+ @media (prefers-reduced-motion: reduce) {
211
+ .rastack-boot-logo { animation: none; }
212
+ .rastack-boot-track::after { animation: none; left: 0; width: 100%; }
213
+ }
197
214
  </style>
198
215
  </head>
199
216
  <body>
200
- <div id="root"><div class="rastack-boot">Loading the WASM engine…</div></div>
217
+ <div id="root"><div class="rastack-boot"><div class="rastack-boot-logo"></div><div class="rastack-boot-track"></div></div></div>
201
218
  <script src="/dev/admin.js"></script>
202
219
  </body>
203
220
  </html>
@@ -21,9 +21,10 @@
21
21
  * but a Rust + wasm toolchain is, it is built from source as a fallback.
22
22
  */
23
23
 
24
- import { execFileSync, spawnSync } from "child_process";
24
+ import { execFileSync, spawn, spawnSync, type ChildProcess } from "child_process";
25
25
  import * as fs from "fs";
26
26
  import * as http from "http";
27
+ import * as net from "net";
27
28
  import * as path from "path";
28
29
  import { runCompile } from "./compile";
29
30
  import {
@@ -31,6 +32,7 @@ import {
31
32
  WASM_ENTRY,
32
33
  appRootCandidates,
33
34
  contentType,
35
+ type DevArgs,
34
36
  parseDevArgs,
35
37
  renderWelcome,
36
38
  wasmBundleCandidates,
@@ -128,8 +130,214 @@ function openBrowser(url: string): void {
128
130
  }
129
131
  }
130
132
 
131
- function main(argv: string[]): void {
133
+ // ---------------------------------------------------------------------------
134
+ // Live-dev mode — `rastack dev` for an Expo / react-native-web app.
135
+ //
136
+ // A React Native (Expo) app has no static `index.html`: its web build is a
137
+ // Metro bundle. Rather than make the developer run a second tool, `dev` starts
138
+ // Expo's Metro dev server itself and reverse-proxies to it, so `rastack dev`
139
+ // stays the single command *and* keeps Metro's fast-refresh. The WASM
140
+ // static-serve path remains the default for apps that ship a built index.html.
141
+ // ---------------------------------------------------------------------------
142
+
143
+ /** True when the project is an Expo app (so `dev` should drive Metro). */
144
+ function isExpoProject(cwd: string): boolean {
145
+ try {
146
+ const appJson = JSON.parse(
147
+ fs.readFileSync(path.join(cwd, "app.json"), "utf-8"),
148
+ );
149
+ if (appJson && appJson.expo) return true;
150
+ } catch {
151
+ /* no app.json — fall through to package.json */
152
+ }
153
+ try {
154
+ const pkg = JSON.parse(
155
+ fs.readFileSync(path.join(cwd, "package.json"), "utf-8"),
156
+ );
157
+ if (pkg?.dependencies?.expo || pkg?.devDependencies?.expo) return true;
158
+ } catch {
159
+ /* no package.json */
160
+ }
161
+ return false;
162
+ }
163
+
164
+ /** Locate the prebuilt wasm bundle without the cargo fallback (optional here). */
165
+ function findWasmDir(): string | null {
166
+ for (const dir of wasmBundleCandidates(HERE)) {
167
+ if (fs.existsSync(path.join(dir, WASM_ENTRY))) return dir;
168
+ }
169
+ return null;
170
+ }
171
+
172
+ /** Grab an ephemeral free TCP port for Metro (avoids a fixed-port clash). */
173
+ function getFreePort(): Promise<number> {
174
+ return new Promise((resolve, reject) => {
175
+ const srv = net.createServer();
176
+ srv.on("error", reject);
177
+ srv.listen(0, "127.0.0.1", () => {
178
+ const addr = srv.address();
179
+ const port = addr && typeof addr === "object" ? addr.port : 0;
180
+ srv.close(() => (port ? resolve(port) : reject(new Error("no free port"))));
181
+ });
182
+ });
183
+ }
184
+
185
+ /** Start `expo start --web` on `port`, with a reload-enabled environment. */
186
+ function startMetro(cwd: string, port: number): ChildProcess {
187
+ // A live dev server needs watch mode, so we must NOT set CI (Expo reads it as
188
+ // "reloads disabled"). BROWSER=none stops Expo opening its own tab — we front
189
+ // it on the rastack port. `shell:true` keeps the npx / npx.cmd split portable.
190
+ const env: NodeJS.ProcessEnv = {
191
+ ...process.env,
192
+ BROWSER: "none",
193
+ EXPO_NO_TELEMETRY: "1",
194
+ };
195
+ delete env.CI;
196
+ // nosemgrep: javascript.lang.security.detect-child-process.detect-child-process
197
+ const child = spawn(`npx expo start --web --port ${port}`, {
198
+ cwd,
199
+ env,
200
+ stdio: ["ignore", "pipe", "pipe"],
201
+ shell: true,
202
+ });
203
+ // Surface Metro's own errors (bundling failures, port issues) with a prefix.
204
+ child.stderr?.on("data", (d) => process.stderr.write(` [metro] ${d}`));
205
+ return child;
206
+ }
207
+
208
+ /** Poll Metro's `/status` until it answers `packager-status:running`. */
209
+ function waitForMetro(port: number, timeoutMs = 120_000): Promise<boolean> {
210
+ const deadline = Date.now() + timeoutMs;
211
+ return new Promise((resolve) => {
212
+ const retry = () =>
213
+ Date.now() > deadline ? resolve(false) : setTimeout(tick, 400);
214
+ const tick = () => {
215
+ const req = http.get(
216
+ { host: "127.0.0.1", port, path: "/status", timeout: 2000 },
217
+ (res) => {
218
+ let body = "";
219
+ res.on("data", (c) => (body += c));
220
+ res.on("end", () =>
221
+ body.includes("packager-status:running") ? resolve(true) : retry(),
222
+ );
223
+ },
224
+ );
225
+ req.on("error", retry);
226
+ req.on("timeout", () => req.destroy());
227
+ };
228
+ tick();
229
+ });
230
+ }
231
+
232
+ /** Reverse-proxy one HTTP request/response pair to Metro. */
233
+ function proxyToMetro(
234
+ req: http.IncomingMessage,
235
+ res: http.ServerResponse,
236
+ port: number,
237
+ ): void {
238
+ const proxyReq = http.request(
239
+ { host: "127.0.0.1", port, method: req.method, path: req.url, headers: req.headers },
240
+ (proxyRes) => {
241
+ res.writeHead(proxyRes.statusCode || 502, proxyRes.headers);
242
+ proxyRes.pipe(res);
243
+ },
244
+ );
245
+ proxyReq.on("error", () => {
246
+ if (!res.headersSent) res.writeHead(502);
247
+ res.end("Metro is not reachable.");
248
+ });
249
+ req.pipe(proxyReq);
250
+ }
251
+
252
+ /** Reverse-proxy a WebSocket upgrade (Metro HMR / fast-refresh) to Metro. */
253
+ function proxyUpgrade(
254
+ req: http.IncomingMessage,
255
+ clientSocket: net.Socket,
256
+ head: Buffer,
257
+ port: number,
258
+ ): void {
259
+ const upstream = net.connect(port, "127.0.0.1", () => {
260
+ const lines = [`${req.method} ${req.url} HTTP/1.1`];
261
+ for (const [k, v] of Object.entries(req.headers)) {
262
+ if (Array.isArray(v)) v.forEach((vv) => lines.push(`${k}: ${vv}`));
263
+ else if (v != null) lines.push(`${k}: ${v}`);
264
+ }
265
+ upstream.write(lines.join("\r\n") + "\r\n\r\n");
266
+ if (head && head.length) upstream.write(head);
267
+ upstream.pipe(clientSocket);
268
+ clientSocket.pipe(upstream);
269
+ });
270
+ upstream.on("error", () => clientSocket.destroy());
271
+ clientSocket.on("error", () => upstream.destroy());
272
+ }
273
+
274
+ /** `rastack dev` for an Expo app: start Metro and reverse-proxy to it. */
275
+ async function runMetroMode(args: DevArgs, cwd: string): Promise<void> {
276
+ // The wasm engine is optional here — an Expo/RN-web app runs its own engine —
277
+ // so a missing bundle is fine. When present, the reserved backend routes work.
278
+ const wasmDir = findWasmDir();
279
+
280
+ const metroPort = await getFreePort();
281
+ console.log(`▸ Starting Metro bundler (Expo) on :${metroPort} …`);
282
+ const metro = startMetro(cwd, metroPort);
283
+
284
+ let shuttingDown = false;
285
+ const stop = (code = 0) => {
286
+ if (shuttingDown) return;
287
+ shuttingDown = true;
288
+ try {
289
+ metro.kill();
290
+ } catch {
291
+ /* already gone */
292
+ }
293
+ process.exit(code);
294
+ };
295
+ process.on("SIGINT", () => stop(0));
296
+ process.on("SIGTERM", () => stop(0));
297
+ metro.on("exit", (code) => {
298
+ if (!shuttingDown) {
299
+ console.error(`\n ✗ Metro exited (code ${code ?? "?"}).`);
300
+ process.exit(code ?? 1);
301
+ }
302
+ });
303
+
304
+ if (!(await waitForMetro(metroPort))) {
305
+ console.error(" ✗ Metro did not come up in time.");
306
+ return stop(1);
307
+ }
308
+
309
+ const server = http.createServer((req, res) => {
310
+ const url = (req.url || "/").split("?")[0];
311
+ // Reserved backend routes win when the engine bundle is available.
312
+ if (wasmDir && url.startsWith("/wasm/"))
313
+ return serveUnder(res, wasmDir, url.slice("/wasm/".length));
314
+ if (url === "/schema.rastack.json" || url === "/openapi.json")
315
+ return serveUnder(res, args.outDir, path.basename(url));
316
+ if (url.startsWith("/data/warehouse/"))
317
+ return serveUnder(res, args.warehouseDir, url.slice("/data/warehouse/".length));
318
+ // Everything else → the live Metro bundle (HTML, JS, assets, HMR polls).
319
+ return proxyToMetro(req, res, metroPort);
320
+ });
321
+ server.on("upgrade", (req, socket, head) =>
322
+ proxyUpgrade(req, socket as net.Socket, head, metroPort),
323
+ );
324
+
325
+ server.listen(args.port, () => {
326
+ const url = `http://localhost:${args.port}`;
327
+ console.log(` ✓ Metro live on :${metroPort} — fast refresh on`);
328
+ if (wasmDir)
329
+ console.log(` ✓ WASM engine: ${path.relative(cwd, wasmDir)}`);
330
+ console.log(`\n ▸ ${url}\n`);
331
+ console.log(
332
+ " Edits reload live. Need the database console? Run `rastack admin`. Ctrl+C to stop.\n",
333
+ );
334
+ if (args.open) openBrowser(url);
335
+ });
336
+ }
337
+
338
+ async function main(argv: string[]): Promise<void> {
132
339
  const args = parseDevArgs(argv);
340
+ const cwd = process.cwd();
133
341
 
134
342
  console.log(`\n rastack dev — your app, in your browser over WASM\n`);
135
343
  console.log(`▸ Compiling ${args.resourcesDir} → ${args.outDir}`);
@@ -138,6 +346,14 @@ function main(argv: string[]): void {
138
346
  process.exit(1);
139
347
  }
140
348
 
349
+ // Serve the developer's own app when there is one; otherwise the welcome page.
350
+ const appRoot = resolveAppRoot(args.appDir);
351
+
352
+ // No static build, but an Expo source tree → drive Metro live (fast refresh).
353
+ if (!appRoot && isExpoProject(cwd)) {
354
+ return runMetroMode(args, cwd);
355
+ }
356
+
141
357
  const wasmDir = resolveWasmDir();
142
358
  if (!wasmDir) {
143
359
  console.error(
@@ -150,9 +366,6 @@ function main(argv: string[]): void {
150
366
  process.exit(1);
151
367
  }
152
368
 
153
- // Serve the developer's own app when there is one; otherwise the welcome page.
154
- const appRoot = resolveAppRoot(args.appDir);
155
-
156
369
  const server = http.createServer((req, res) => {
157
370
  const url = (req.url || "/").split("?")[0];
158
371
  if (req.method !== "GET") {
@@ -212,5 +425,8 @@ function main(argv: string[]): void {
212
425
  }
213
426
 
214
427
  if (require.main === module) {
215
- main(process.argv.slice(2));
428
+ main(process.argv.slice(2)).catch((err) => {
429
+ console.error(err);
430
+ process.exit(1);
431
+ });
216
432
  }
Binary file