rastack 0.0.63 → 0.0.69

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 (76) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +19 -24
  3. package/admin/AdminApp.tsx +56 -3
  4. package/admin/httpEngine.ts +5 -6
  5. package/admin/index.tsx +11 -85
  6. package/admin/ui/icons.tsx +9 -0
  7. package/admin/ui/styles.ts +3 -0
  8. package/admin/useAdmin.ts +41 -4
  9. package/dist/admin.js +17 -22
  10. package/dist/compile/analyze.js +11 -1
  11. package/dist/compile/model.d.ts +1 -1
  12. package/dist/compile/run.d.ts +2 -2
  13. package/dist/compile/run.js +2 -2
  14. package/dist/define/index.d.ts +10 -4
  15. package/dist/define/index.js +2 -2
  16. package/dist/define/manifest.js +7 -1
  17. package/dist/deploy/io.d.ts +50 -0
  18. package/dist/deploy/io.js +124 -0
  19. package/dist/deploy/workflows.js +1 -1
  20. package/dist/dev/harness.d.ts +19 -27
  21. package/dist/dev/harness.js +31 -40
  22. package/dist/dev/native-server.d.ts +29 -0
  23. package/dist/dev/native-server.js +174 -0
  24. package/dist/external/envelope.d.ts +1 -1
  25. package/dist/external/envelope.js +1 -1
  26. package/dist/external/service.d.ts +2 -2
  27. package/dist/external/service.js +2 -2
  28. package/dist/external/types.d.ts +1 -1
  29. package/dist/external/types.js +1 -1
  30. package/dist/rastack-admin.d.ts +6 -14
  31. package/dist/rastack-admin.js +60 -88
  32. package/dist/rastack-bootstrap.js +18 -47
  33. package/dist/rastack-design.d.ts +1 -2
  34. package/dist/rastack-design.js +1 -2
  35. package/dist/rastack-dev.d.ts +7 -10
  36. package/dist/rastack-dev.js +68 -80
  37. package/dist/rastack-init.d.ts +6 -3
  38. package/dist/rastack-init.js +43 -30
  39. package/dist/rastack.d.ts +1 -1
  40. package/dist/rastack.js +5 -6
  41. package/dist/validate/transitions.d.ts +1 -1
  42. package/dist/validate/transitions.js +1 -1
  43. package/hooks/query/api.ts +4 -6
  44. package/package.json +2 -2
  45. package/provider/index.ts +0 -1
  46. package/provider/provider.tsx +31 -134
  47. package/provider/types.ts +30 -69
  48. package/src/compile/analyze.ts +11 -1
  49. package/src/compile/model.ts +1 -1
  50. package/src/compile/run.ts +2 -2
  51. package/src/define/index.ts +7 -4
  52. package/src/define/manifest.ts +7 -1
  53. package/src/deploy/io.ts +162 -1
  54. package/src/deploy/workflows.ts +1 -1
  55. package/src/dev/harness.ts +30 -43
  56. package/src/dev/native-server.ts +164 -0
  57. package/src/external/envelope.ts +1 -1
  58. package/src/external/service.ts +2 -2
  59. package/src/external/types.ts +1 -1
  60. package/src/rastack-admin.ts +66 -107
  61. package/src/rastack-bootstrap.ts +24 -69
  62. package/src/rastack-design.ts +1 -2
  63. package/src/rastack-dev.ts +79 -91
  64. package/src/rastack-init.ts +50 -41
  65. package/src/rastack.ts +5 -6
  66. package/src/validate/transitions.ts +1 -1
  67. package/dist/rastack-wasm-build.d.ts +0 -19
  68. package/dist/rastack-wasm-build.js +0 -94
  69. package/dist/wasm/package.json +0 -4
  70. package/dist/wasm/rastack_wasm.d.ts +0 -100
  71. package/dist/wasm/rastack_wasm.js +0 -745
  72. package/dist/wasm/rastack_wasm_bg.wasm +0 -0
  73. package/dist/wasm/rastack_wasm_bg.wasm.d.ts +0 -28
  74. package/provider/wasm.ts +0 -126
  75. package/scripts/copy-wasm.js +0 -35
  76. package/src/rastack-wasm-build.ts +0 -113
@@ -1,81 +1,29 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * `rastack admin` — the full, Django-admin-style console, served **in the
5
- * browser over the prebuilt WebAssembly engine** (exactly like `rastack dev`).
4
+ * `rastack admin` — the Django-admin-style console, served by the native
5
+ * `rastack-server` process.
6
6
  *
7
7
  * rastack admin [resourcesDir] [--warehouse <dir>] [--port <n>] [--out <dir>] [--no-open]
8
8
  *
9
- * The console is React API Stack's own React admin (`tools/admin`, built from
10
- * `rastack/components` + `rastack/theme`). This command compiles the resources,
11
- * then boots a dependency-free static server that hands the browser everything
12
- * it needs to run the admin locally over WASM — no native server, no cargo:
13
- * • the bundled React admin (`/dev/admin.js`), which defaults to `mode:"local"`,
14
- * • the prebuilt WASM engine (`/wasm/…`) shipped in the package,
15
- * • the compiled `schema.rastack.json` / `openapi.json`,
16
- * • the committed Iceberg warehouse (`/data/warehouse/…`) to seed from.
17
- *
18
- * The engine is the single prebuilt `.wasm` bundle shipped in the npm package;
19
- * if it is missing the command errors and points at `rastack update` — it never
20
- * falls back to a from-source cargo build.
9
+ * Compiles the resources, then starts `rastack-server admin`. That process is
10
+ * the API (`rastack-api-core`) and it serves the React bundle at `/admin/`.
11
+ * The browser renders the console and calls `/api` on that same origin.
12
+ * The engine does not run in the browser.
21
13
  */
22
14
 
23
15
  import { execFileSync } from "child_process";
24
16
  import * as fs from "fs";
25
- import * as http from "http";
26
17
  import * as path from "path";
27
18
  import { runCompile } from "./compile";
19
+ import { parseAdminArgs } from "./dev/harness";
28
20
  import {
29
- WASM_ENTRY,
30
- contentType,
31
- parseAdminArgs,
32
- renderShell,
33
- wasmBundleCandidates,
34
- } from "./dev/harness";
35
-
36
- const HERE = __dirname; // dist/ when installed — the bundle lives beside us.
37
-
38
- /** Locate the prebuilt wasm bundle dir shipped in the package (no cargo fallback). */
39
- function resolveWasmDir(): string | null {
40
- for (const dir of wasmBundleCandidates(HERE)) {
41
- if (fs.existsSync(path.join(dir, WASM_ENTRY))) return dir;
42
- }
43
- return null;
44
- }
21
+ ensureServerBinary,
22
+ startNativeServer,
23
+ waitForTcp,
24
+ } from "./dev/native-server";
45
25
 
46
- /** Serve one file if it exists and stays within `root`; else 404/403. */
47
- function serveUnder(
48
- res: http.ServerResponse,
49
- root: string,
50
- relPath: string,
51
- ): void {
52
- const safeRoot = path.resolve(root);
53
- const rel = decodeURIComponent(relPath).replace(/^\/+/, "");
54
- const target = path.resolve(safeRoot, rel);
55
- // Path-traversal guard: the resolved target must stay inside the root.
56
- if (target !== safeRoot && !target.startsWith(safeRoot + path.sep)) {
57
- res.writeHead(403).end("Forbidden");
58
- return;
59
- }
60
- fs.readFile(target, (err, data) => {
61
- if (err) {
62
- res.writeHead(404).end("Not found");
63
- return;
64
- }
65
- res.writeHead(200, { "Content-Type": contentType(target) }).end(data);
66
- });
67
- }
68
-
69
- /** Serve an exact file (no traversal concern — a fixed internal artifact). */
70
- function serveFile(res: http.ServerResponse, file: string): void {
71
- fs.readFile(file, (err, data) => {
72
- if (err) {
73
- res.writeHead(404).end("Not found");
74
- return;
75
- }
76
- res.writeHead(200, { "Content-Type": contentType(file) }).end(data);
77
- });
78
- }
26
+ const HERE = __dirname; // dist/ when installed — admin.js lives beside us.
79
27
 
80
28
  /** Best-effort open the default browser at `url`. */
81
29
  function openBrowser(url: string): void {
@@ -92,11 +40,12 @@ function openBrowser(url: string): void {
92
40
  }
93
41
  }
94
42
 
95
- function main(argv: string[]): void {
43
+ async function main(argv: string[]): Promise<void> {
96
44
  const args = parseAdminArgs(argv);
45
+ const cwd = process.cwd();
97
46
 
98
47
  console.log(
99
- `\n rastack admin — the database console, built with React API Stack\n`,
48
+ `\n rastack admin — the database console, on the native Rust server\n`,
100
49
  );
101
50
  console.log(`▸ Compiling ${args.resourcesDir} → ${args.outDir}`);
102
51
  if (!runCompile(args.resourcesDir, args.outDir)) {
@@ -104,16 +53,6 @@ function main(argv: string[]): void {
104
53
  process.exit(1);
105
54
  }
106
55
 
107
- const wasmDir = resolveWasmDir();
108
- if (!wasmDir) {
109
- console.error(
110
- "\n ✗ No prebuilt WASM engine found in this rastack install.\n" +
111
- " Update rastack to pull the prebuilt bundle:\n" +
112
- " rastack update\n",
113
- );
114
- process.exit(1);
115
- }
116
-
117
56
  const adminBundle = path.join(HERE, "admin.js");
118
57
  if (!fs.existsSync(adminBundle)) {
119
58
  console.error(
@@ -122,41 +61,61 @@ function main(argv: string[]): void {
122
61
  process.exit(1);
123
62
  }
124
63
 
125
- const server = http.createServer((req, res) => {
126
- const url = (req.url || "/").split("?")[0];
127
- if (req.method !== "GET") {
128
- res.writeHead(405).end("Method not allowed");
129
- return;
130
- }
131
- if (url === "/" || url === "/index.html") {
132
- res
133
- .writeHead(200, { "Content-Type": "text/html; charset=utf-8" })
134
- .end(renderShell("React API Stack — admin"));
135
- return;
136
- }
137
- if (url === "/dev/admin.js") return serveFile(res, adminBundle);
138
- if (url.startsWith("/wasm/"))
139
- return serveUnder(res, wasmDir, url.slice("/wasm/".length));
140
- if (url === "/schema.rastack.json" || url === "/openapi.json")
141
- return serveUnder(res, args.outDir, path.basename(url));
142
- if (url.startsWith("/data/warehouse/"))
143
- return serveUnder(
144
- res,
145
- args.warehouseDir,
146
- url.slice("/data/warehouse/".length),
147
- );
148
- res.writeHead(404).end("Not found");
64
+ let bin: string;
65
+ try {
66
+ bin = ensureServerBinary(cwd);
67
+ } catch (err) {
68
+ console.error(`\n${(err as Error).message}\n`);
69
+ process.exit(1);
70
+ }
71
+
72
+ const manifest = path.resolve(cwd, args.outDir, "schema.rastack.json");
73
+ const warehouse = path.resolve(cwd, args.warehouseDir);
74
+ const child = startNativeServer({
75
+ bin,
76
+ command: "admin",
77
+ manifest,
78
+ warehouse,
79
+ addr: `127.0.0.1:${args.port}`,
80
+ adminAssets: HERE,
149
81
  });
150
82
 
151
- server.listen(args.port, () => {
152
- const url = `http://localhost:${args.port}`;
153
- console.log(` ✓ WASM engine: ${path.relative(process.cwd(), wasmDir)}`);
154
- console.log(`\n ▸ ${url}\n`);
155
- console.log(" Press Ctrl+C to stop.\n");
156
- if (args.open) openBrowser(url);
83
+ const stop = () => {
84
+ try {
85
+ child.kill("SIGTERM");
86
+ } catch {
87
+ /* already gone */
88
+ }
89
+ };
90
+ process.on("SIGINT", () => {
91
+ stop();
92
+ process.exit(0);
93
+ });
94
+ process.on("SIGTERM", () => {
95
+ stop();
96
+ process.exit(0);
157
97
  });
98
+
99
+ try {
100
+ await waitForTcp(args.port, child);
101
+ } catch (err) {
102
+ console.error(`\n${(err as Error).message}\n`);
103
+ stop();
104
+ process.exit(1);
105
+ }
106
+
107
+ const url = `http://127.0.0.1:${args.port}/admin/`;
108
+ console.log(` ✓ native engine: ${bin}`);
109
+ console.log(`\n ▸ ${url}\n`);
110
+ console.log(" Press Ctrl+C to stop.\n");
111
+ if (args.open) openBrowser(url);
112
+
113
+ child.on("exit", (code) => process.exit(code ?? 0));
158
114
  }
159
115
 
160
116
  if (require.main === module) {
161
- main(process.argv.slice(2));
117
+ main(process.argv.slice(2)).catch((err) => {
118
+ console.error(err);
119
+ process.exit(1);
120
+ });
162
121
  }
@@ -25,11 +25,8 @@
25
25
  */
26
26
 
27
27
  import {
28
- BOOTSTRAP_ARTIFACTS_OUTPUT,
29
- BOOTSTRAP_TEMPLATES_OUTPUT,
30
28
  DeployConfig,
31
29
  PartialDeployConfig,
32
- bootstrapStackArgs,
33
30
  bootstrapStackCommand,
34
31
  bootstrapStackName,
35
32
  mergeConfig,
@@ -40,12 +37,12 @@ import {
40
37
  import {
41
38
  ask,
42
39
  awsAvailable,
43
- awsRun,
40
+ awsIdentity,
44
41
  canPrompt,
42
+ ensureBootstrapStack,
45
43
  gitRemoteUrl,
46
44
  loadConfig,
47
45
  resolveDeployAsset,
48
- stackOutput,
49
46
  uploadPlaceholder,
50
47
  withPrompt,
51
48
  } from "./deploy/io";
@@ -164,47 +161,12 @@ function printPlan(cfg: DeployConfig, reuseOidc: boolean): void {
164
161
  }
165
162
 
166
163
  /**
167
- * Deploy the bootstrap stack, retrying once against an account that already has
168
- * a GitHub OIDC provider (AWS allows only one per URL) when the caller did not
169
- * already ask to reuse it.
164
+ * Stand the tier-0 bootstrap stack up and upload the Lambda placeholder. The
165
+ * deploy (with the GitHub-OIDC-reuse retry) and template resolution are shared
166
+ * with `rastack init` via `ensureBootstrapStack`, so both stand the stack up
167
+ * identically.
170
168
  */
171
- function deployBootstrap(cfg: DeployConfig, reuseOidc: boolean): void {
172
- const template = resolveDeployAsset("bootstrap.yaml");
173
- if (!template) {
174
- console.error(
175
- " ✗ bootstrap.yaml template not found in the rastack package. Reinstall\n" +
176
- " rastack (`npm i -g rastack`) or run from the framework repo.",
177
- );
178
- process.exit(1);
179
- }
180
- try {
181
- awsRun(bootstrapStackArgs(cfg, reuseOidc, template));
182
- } catch (err) {
183
- if (reuseOidc) {
184
- console.error(
185
- `\n ✗ bootstrap stack deploy failed.\n ${(err as Error).message}`,
186
- );
187
- process.exit(1);
188
- }
189
- // The most common cause is an account that already has a GitHub OIDC
190
- // provider. Retry telling the template to reuse it instead of creating a
191
- // second (which CloudFormation rejects).
192
- console.log(
193
- " • retrying bootstrap, reusing the account's existing GitHub OIDC provider",
194
- );
195
- try {
196
- awsRun(bootstrapStackArgs(cfg, true, template));
197
- } catch (retryErr) {
198
- console.error(
199
- `\n ✗ bootstrap stack deploy failed.\n ${(retryErr as Error).message}`,
200
- );
201
- process.exit(1);
202
- }
203
- }
204
- }
205
-
206
- /** Stand the tier-0 bootstrap stack up and upload the Lambda placeholder. */
207
- function standUp(cfg: DeployConfig, reuseOidc: boolean): void {
169
+ async function standUp(cfg: DeployConfig, reuseOidc: boolean): Promise<void> {
208
170
  if (!awsAvailable()) {
209
171
  console.error(
210
172
  " ✗ the AWS CLI is not on PATH. Install it and configure credentials,\n" +
@@ -213,33 +175,26 @@ function standUp(cfg: DeployConfig, reuseOidc: boolean): void {
213
175
  process.exit(1);
214
176
  }
215
177
 
216
- const stack = bootstrapStackName(cfg);
217
- const existing = stackOutput(stack, BOOTSTRAP_ARTIFACTS_OUTPUT, cfg.region);
218
- if (existing) {
219
- console.log(
220
- `\n▸ Bootstrap stack ${stack} already exists — reconciling (idempotent).`,
221
- );
222
- } else {
223
- console.log(`\n▸ Creating bootstrap stack ${stack} (@ ${cfg.region})`);
224
- }
225
- deployBootstrap(cfg, reuseOidc);
226
-
227
- const artifactsBucket = stackOutput(
228
- stack,
229
- BOOTSTRAP_ARTIFACTS_OUTPUT,
230
- cfg.region,
231
- );
232
- const templatesBucket = stackOutput(
233
- stack,
234
- BOOTSTRAP_TEMPLATES_OUTPUT,
235
- cfg.region,
236
- );
237
- if (!artifactsBucket) {
178
+ // Verify the AWS CLI is logged in before attempting a stack deploy.
179
+ const account = awsIdentity(cfg.region);
180
+ if (!account) {
238
181
  console.error(
239
- " ✗ bootstrap stack deployed but its ArtifactsBucket output could not be read.",
182
+ " ✗ the AWS CLI has no usable credentials — it is not logged in.\n" +
183
+ " Run `aws configure` (or `aws sso login`), then re-run `rastack bootstrap`.\n" +
184
+ " Or re-run with --print to see the command.",
240
185
  );
241
186
  process.exit(1);
242
187
  }
188
+ console.log(` ✓ AWS account ${account} (${cfg.region})`);
189
+
190
+ const stack = bootstrapStackName(cfg);
191
+ const { artifactsBucket, templatesBucket } = await ensureBootstrapStack(
192
+ cfg,
193
+ reuseOidc,
194
+ ).catch((err: Error) => {
195
+ console.error(`\n ✗ ${err.message}`);
196
+ process.exit(1);
197
+ });
243
198
  console.log(` ✓ artifacts bucket: ${artifactsBucket}`);
244
199
  if (templatesBucket) console.log(` ✓ templates bucket: ${templatesBucket}`);
245
200
 
@@ -299,7 +254,7 @@ async function main(argv: string[]): Promise<void> {
299
254
  }
300
255
  }
301
256
 
302
- standUp(cfg, flags.reuseOidc);
257
+ await standUp(cfg, flags.reuseOidc);
303
258
  }
304
259
 
305
260
  if (require.main === module) {
@@ -12,8 +12,7 @@
12
12
  * source and hitting **Save** writes it back and recompiles `tokens.css` /
13
13
  * `tokens.ts`, so the rest of the app picks the change up on reload.
14
14
  *
15
- * In keeping with the local-first ethos (the whole API runs in the browser via
16
- * WASM), the studio needs no build step and no external dependencies.
15
+ * The studio needs no build step and no external dependencies.
17
16
  */
18
17
 
19
18
  import * as http from "http";
@@ -1,32 +1,24 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * `rastack dev` — local development of **your app**, always over the WASM engine.
4
+ * `rastack dev` — local development of **your app** against the native engine.
5
5
  *
6
6
  * rastack dev [resourcesDir] [--port <n>] [--warehouse <dir>] [--out <dir>] [--app <dir>] [--no-open]
7
7
  *
8
- * Compiles the resources, then boots a dependency-free static server (à la
9
- * `rastack design`) that hands the browser everything it needs to run the
10
- * developer's own app locally over WebAssembly — no native server, no cargo:
8
+ * Compiles the resources, starts a native `rastack-server` (`rastack-api-core`
9
+ * over the on-disk Iceberg warehouse), and serves the developer's app. The
10
+ * browser is the UI. `/api` on this origin is proxied to that process.
11
11
  * • the developer's app (`index.html` from the resolved app root), or a
12
12
  * Django-style welcome page when the project has no app yet,
13
- * • the prebuilt WASM engine (`/wasm/…`) shipped in the package,
14
13
  * • the compiled `schema.rastack.json` / `openapi.json`,
15
- * • the committed Iceberg warehouse (`/data/warehouse/…`) to seed from.
14
+ * • `/api/…` forwarded to the native server.
16
15
  *
17
16
  * It does **not** serve the admin console — that is a separate command,
18
- * `rastack admin`.
19
- *
20
- * The engine is resolved from the package's prebuilt bundle; if none is present
21
- * but a Rust + wasm toolchain is, it is built from source as a fallback.
17
+ * `rastack admin`. The browser does not execute the engine. There is no
18
+ * in-browser fallback.
22
19
  */
23
20
 
24
- import {
25
- execFileSync,
26
- spawn,
27
- spawnSync,
28
- type ChildProcess,
29
- } from "child_process";
21
+ import { execFileSync, spawn, type ChildProcess } from "child_process";
30
22
  import * as fs from "fs";
31
23
  import * as http from "http";
32
24
  import * as net from "net";
@@ -36,25 +28,17 @@ import { cognitoRuntimeConfig } from "./deploy/config";
36
28
  import { loadConfig } from "./deploy/io";
37
29
  import {
38
30
  APP_ENTRY,
39
- WASM_ENTRY,
40
31
  appRootCandidates,
41
32
  contentType,
42
33
  type DevArgs,
43
34
  parseDevArgs,
44
35
  renderWelcome,
45
- wasmBundleCandidates,
46
36
  } from "./dev/harness";
47
-
48
- const HERE = __dirname; // dist/ when installed
49
-
50
- /** True when `cargo` is on PATH (the from-source wasm fallback needs it). */
51
- function cargoAvailable(): boolean {
52
- try {
53
- return spawnSync("cargo", ["--version"], { stdio: "ignore" }).status === 0;
54
- } catch {
55
- return false;
56
- }
57
- }
37
+ import {
38
+ ensureServerBinary,
39
+ startNativeServer,
40
+ waitForTcp,
41
+ } from "./dev/native-server";
58
42
 
59
43
  /** First app-root candidate that holds an `index.html`, or null (→ welcome). */
60
44
  function resolveAppRoot(appDir: string): string | null {
@@ -64,30 +48,6 @@ function resolveAppRoot(appDir: string): string | null {
64
48
  return null;
65
49
  }
66
50
 
67
- /** Locate the prebuilt wasm bundle dir, building from source as a fallback. */
68
- function resolveWasmDir(): string | null {
69
- for (const dir of wasmBundleCandidates(HERE)) {
70
- if (fs.existsSync(path.join(dir, WASM_ENTRY))) return dir;
71
- }
72
- if (cargoAvailable()) {
73
- console.log(
74
- "\n▸ No prebuilt WASM engine found — building it from source (cargo)…\n",
75
- );
76
- try {
77
- // Lazy require so a normal `dev` with a prebuilt bundle never loads it.
78
- // eslint-disable-next-line @typescript-eslint/no-var-requires
79
- require("./rastack-wasm-build").buildWasm();
80
- } catch (err) {
81
- console.error(` ✗ wasm build failed: ${(err as Error).message}`);
82
- return null;
83
- }
84
- for (const dir of wasmBundleCandidates(HERE)) {
85
- if (fs.existsSync(path.join(dir, WASM_ENTRY))) return dir;
86
- }
87
- }
88
- return null;
89
- }
90
-
91
51
  /** Serve one file if it exists and stays within `root`; else 404/403. */
92
52
  function serveUnder(
93
53
  res: http.ServerResponse,
@@ -174,8 +134,9 @@ function openBrowser(url: string): void {
174
134
  // A React Native (Expo) app has no static `index.html`: its web build is a
175
135
  // Metro bundle. Rather than make the developer run a second tool, `dev` starts
176
136
  // Expo's Metro dev server itself and reverse-proxies to it, so `rastack dev`
177
- // stays the single command *and* keeps Metro's fast-refresh. The WASM
178
- // static-serve path remains the default for apps that ship a built index.html.
137
+ // stays the single command *and* keeps Metro's fast-refresh. Apps that ship
138
+ // a built index.html use the static server below. Both proxy /api to the
139
+ // native server.
179
140
  // ---------------------------------------------------------------------------
180
141
 
181
142
  /** True when the project is an Expo app (so `dev` should drive Metro). */
@@ -199,15 +160,7 @@ function isExpoProject(cwd: string): boolean {
199
160
  return false;
200
161
  }
201
162
 
202
- /** Locate the prebuilt wasm bundle without the cargo fallback (optional here). */
203
- function findWasmDir(): string | null {
204
- for (const dir of wasmBundleCandidates(HERE)) {
205
- if (fs.existsSync(path.join(dir, WASM_ENTRY))) return dir;
206
- }
207
- return null;
208
- }
209
-
210
- /** Grab an ephemeral free TCP port for Metro (avoids a fixed-port clash). */
163
+ /** Grab an ephemeral free TCP port (Metro, or the native API beside this server). */
211
164
  function getFreePort(): Promise<number> {
212
165
  return new Promise((resolve, reject) => {
213
166
  const srv = net.createServer();
@@ -295,7 +248,7 @@ function proxyToMetro(
295
248
  );
296
249
  proxyReq.on("error", () => {
297
250
  if (!res.headersSent) res.writeHead(502);
298
- res.end("Metro is not reachable.");
251
+ res.end("Upstream is not reachable.");
299
252
  });
300
253
  req.pipe(proxyReq);
301
254
  }
@@ -323,11 +276,12 @@ function proxyUpgrade(
323
276
  }
324
277
 
325
278
  /** `rastack dev` for an Expo app: start Metro and reverse-proxy to it. */
326
- async function runMetroMode(args: DevArgs, cwd: string): Promise<void> {
327
- // The wasm engine is optional here — an Expo/RN-web app runs its own engine —
328
- // so a missing bundle is fine. When present, the reserved backend routes work.
329
- const wasmDir = findWasmDir();
330
-
279
+ async function runMetroMode(
280
+ args: DevArgs,
281
+ cwd: string,
282
+ apiPort: number,
283
+ stopEngine: () => void,
284
+ ): Promise<void> {
331
285
  const metroPort = await getFreePort();
332
286
  console.log(`▸ Starting Metro bundler (Expo) on :${metroPort} …`);
333
287
  const metro = startMetro(cwd, metroPort);
@@ -336,6 +290,7 @@ async function runMetroMode(args: DevArgs, cwd: string): Promise<void> {
336
290
  const stop = (code = 0) => {
337
291
  if (shuttingDown) return;
338
292
  shuttingDown = true;
293
+ stopEngine();
339
294
  try {
340
295
  metro.kill();
341
296
  } catch {
@@ -359,9 +314,8 @@ async function runMetroMode(args: DevArgs, cwd: string): Promise<void> {
359
314
 
360
315
  const server = http.createServer((req, res) => {
361
316
  const url = (req.url || "/").split("?")[0];
362
- // Reserved backend routes win when the engine bundle is available.
363
- if (wasmDir && url.startsWith("/wasm/"))
364
- return serveUnder(res, wasmDir, url.slice("/wasm/".length));
317
+ if (url.startsWith("/api/") || url === "/api")
318
+ return proxyToMetro(req, res, apiPort);
365
319
  if (url === "/schema.rastack.json" || url === "/openapi.json")
366
320
  return serveUnder(res, args.outDir, path.basename(url));
367
321
  if (url === "/__rastack/cognito.json") return serveDevCognito(res, cwd);
@@ -392,7 +346,7 @@ async function runMetroMode(args: DevArgs, cwd: string): Promise<void> {
392
346
  server.listen(args.port, () => {
393
347
  const url = `http://localhost:${args.port}`;
394
348
  console.log(` ✓ Metro live on :${metroPort} — fast refresh on`);
395
- if (wasmDir) console.log(` ✓ WASM engine: ${path.relative(cwd, wasmDir)}`);
349
+ console.log(` ✓ native engine: 127.0.0.1:${apiPort} (proxied at /api)`);
396
350
  console.log(`\n ▸ ${url}\n`);
397
351
  console.log(
398
352
  " Edits reload live. Need the database console? Run `rastack admin`. Ctrl+C to stop.\n",
@@ -405,7 +359,9 @@ async function main(argv: string[]): Promise<void> {
405
359
  const args = parseDevArgs(argv);
406
360
  const cwd = process.cwd();
407
361
 
408
- console.log(`\n rastack dev — your app, in your browser over WASM\n`);
362
+ console.log(
363
+ `\n rastack dev — your app in the browser, the API in native Rust\n`,
364
+ );
409
365
  console.log(`▸ Compiling ${args.resourcesDir} → ${args.outDir}`);
410
366
  if (!runCompile(args.resourcesDir, args.outDir)) {
411
367
  console.error(" ✗ compile failed — fix the resource graph and re-run.");
@@ -415,32 +371,64 @@ async function main(argv: string[]): Promise<void> {
415
371
  // Serve the developer's own app when there is one; otherwise the welcome page.
416
372
  const appRoot = resolveAppRoot(args.appDir);
417
373
 
374
+ let engine: ChildProcess;
375
+ let apiPort: number;
376
+ let stopping = false;
377
+ try {
378
+ const bin = ensureServerBinary(cwd);
379
+ apiPort = await getFreePort();
380
+ const manifest = path.resolve(cwd, args.outDir, "schema.rastack.json");
381
+ const warehouse = path.resolve(cwd, args.warehouseDir);
382
+ engine = startNativeServer({
383
+ bin,
384
+ command: "serve",
385
+ manifest,
386
+ warehouse,
387
+ addr: `127.0.0.1:${apiPort}`,
388
+ });
389
+ await waitForTcp(apiPort, engine);
390
+ } catch (err) {
391
+ console.error(`\n${(err as Error).message}\n`);
392
+ process.exit(1);
393
+ }
394
+
395
+ const stopEngine = () => {
396
+ stopping = true;
397
+ try {
398
+ engine.kill("SIGTERM");
399
+ } catch {
400
+ /* already gone */
401
+ }
402
+ };
403
+ engine.on("exit", (code) => {
404
+ if (stopping) return;
405
+ console.error(`\n ✗ native engine exited (code ${code ?? "?"}).`);
406
+ process.exit(code ?? 1);
407
+ });
408
+
418
409
  // No static build, but an Expo source tree → drive Metro live (fast refresh).
410
+ // Metro mode owns the shutdown signals so it can stop both processes.
419
411
  if (!appRoot && isExpoProject(cwd)) {
420
- return runMetroMode(args, cwd);
412
+ return runMetroMode(args, cwd, apiPort, stopEngine);
421
413
  }
422
414
 
423
- const wasmDir = resolveWasmDir();
424
- if (!wasmDir) {
425
- console.error(
426
- "\n ✗ No WASM engine available.\n" +
427
- " Upgrade rastack for the prebuilt bundle (`rastack update`), or install\n" +
428
- " the Rust toolchain to build it from source:\n" +
429
- " rustup target add wasm32-unknown-unknown\n" +
430
- " cargo install wasm-bindgen-cli\n",
431
- );
432
- process.exit(1);
433
- }
415
+ process.on("SIGINT", () => {
416
+ stopEngine();
417
+ process.exit(0);
418
+ });
419
+ process.on("SIGTERM", () => {
420
+ stopEngine();
421
+ process.exit(0);
422
+ });
434
423
 
435
424
  const server = http.createServer((req, res) => {
436
425
  const url = (req.url || "/").split("?")[0];
426
+ if (url.startsWith("/api/") || url === "/api")
427
+ return proxyToMetro(req, res, apiPort);
437
428
  if (req.method !== "GET") {
438
429
  res.writeHead(405).end("Method not allowed");
439
430
  return;
440
431
  }
441
- // Reserved backend routes always win over the app's static files.
442
- if (url.startsWith("/wasm/"))
443
- return serveUnder(res, wasmDir, url.slice("/wasm/".length));
444
432
  if (url === "/schema.rastack.json" || url === "/openapi.json")
445
433
  return serveUnder(res, args.outDir, path.basename(url));
446
434
  if (url === "/__rastack/cognito.json") return serveDevCognito(res, cwd);
@@ -479,7 +467,7 @@ async function main(argv: string[]): Promise<void> {
479
467
 
480
468
  server.listen(args.port, () => {
481
469
  const url = `http://localhost:${args.port}`;
482
- console.log(` ✓ WASM engine: ${path.relative(process.cwd(), wasmDir)}`);
470
+ console.log(` ✓ native engine: 127.0.0.1:${apiPort} (proxied at /api)`);
483
471
  if (appRoot)
484
472
  console.log(` ✓ app: ${path.relative(process.cwd(), appRoot) || "."}`);
485
473
  else console.log(` ○ no app found — serving the welcome page`);