rastack 0.0.56 → 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.
@@ -4,6 +4,7 @@ export { analyze, analyzeDsl } from "./analyze";
4
4
  export { analyzeEntities, detectCycles } from "./entities";
5
5
  export type { EntityAnalysis, EntityDiagnostic, EntityTypeExport, } from "./entities";
6
6
  export { buildManifest } from "./manifest";
7
+ export { runCompile } from "./run";
7
8
  export { buildOpenApi, writeSchema } from "./openapi";
8
9
  export * from "./model";
9
10
  export interface CompileResult {
@@ -36,7 +36,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
36
36
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
- exports.writeSchema = exports.buildOpenApi = exports.buildManifest = exports.detectCycles = exports.analyzeEntities = exports.analyzeDsl = exports.analyze = void 0;
39
+ exports.writeSchema = exports.buildOpenApi = exports.runCompile = exports.buildManifest = exports.detectCycles = exports.analyzeEntities = exports.analyzeDsl = exports.analyze = void 0;
40
40
  exports.resolveResourceFiles = resolveResourceFiles;
41
41
  exports.analyzeProject = analyzeProject;
42
42
  exports.detectTransitionProblems = detectTransitionProblems;
@@ -61,6 +61,8 @@ Object.defineProperty(exports, "analyzeEntities", { enumerable: true, get: funct
61
61
  Object.defineProperty(exports, "detectCycles", { enumerable: true, get: function () { return entities_2.detectCycles; } });
62
62
  var manifest_2 = require("./manifest");
63
63
  Object.defineProperty(exports, "buildManifest", { enumerable: true, get: function () { return manifest_2.buildManifest; } });
64
+ var run_1 = require("./run");
65
+ Object.defineProperty(exports, "runCompile", { enumerable: true, get: function () { return run_1.runCompile; } });
64
66
  var openapi_2 = require("./openapi");
65
67
  Object.defineProperty(exports, "buildOpenApi", { enumerable: true, get: function () { return openapi_2.buildOpenApi; } });
66
68
  Object.defineProperty(exports, "writeSchema", { enumerable: true, get: function () { return openapi_2.writeSchema; } });
@@ -96,7 +98,8 @@ function resolveResourceFiles(input) {
96
98
  if (!IGNORE_DIRS.has(entry.name))
97
99
  walk(path.join(dir, entry.name));
98
100
  }
99
- else if (/\.tsx?$/.test(entry.name) && !entry.name.endsWith(".d.ts")) {
101
+ else if (/\.tsx?$/.test(entry.name) &&
102
+ !entry.name.endsWith(".d.ts")) {
100
103
  files.push(path.resolve(dir, entry.name));
101
104
  }
102
105
  }
@@ -124,7 +127,9 @@ function analyzeProject(input) {
124
127
  ...fromDsl.resources,
125
128
  ...fromInterfaces.resources.filter((r) => !defined.has(`${r.app}.${r.model}`)),
126
129
  ];
127
- resources.sort((a, b) => a.app === b.app ? a.model.localeCompare(b.model) : a.app.localeCompare(b.app));
130
+ resources.sort((a, b) => a.app === b.app
131
+ ? a.model.localeCompare(b.model)
132
+ : a.app.localeCompare(b.app));
128
133
  // Registry entries follow the same merge rule as the resources: the DSL
129
134
  // definition wins an `app.model` collision.
130
135
  const types = [
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Run the compiler **in-process**, writing `schema.rastack.json` +
3
+ * `openapi.json` (and `rastack-env.d.ts`) into `outDir`. Prints any warnings
4
+ * and, on failure, the error; returns `true` on success.
5
+ *
6
+ * This is the single entry point the two real modalities of use share instead
7
+ * of shelling out to a standalone `compile` command:
8
+ * • `rastack dev` / `rastack admin` — local development (compile, then serve
9
+ * the app + admin in the browser over WASM), and
10
+ * • `rastack ci` — the deploy pipeline (compile the manifest the Rust backend
11
+ * reads + the OpenAPI contract, then build/ship).
12
+ */
13
+ export declare function runCompile(resourcesDir: string, outDir: string): boolean;
@@ -0,0 +1,63 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.runCompile = runCompile;
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ const index_1 = require("./index");
40
+ /**
41
+ * Run the compiler **in-process**, writing `schema.rastack.json` +
42
+ * `openapi.json` (and `rastack-env.d.ts`) into `outDir`. Prints any warnings
43
+ * and, on failure, the error; returns `true` on success.
44
+ *
45
+ * This is the single entry point the two real modalities of use share instead
46
+ * of shelling out to a standalone `compile` command:
47
+ * • `rastack dev` / `rastack admin` — local development (compile, then serve
48
+ * the app + admin in the browser over WASM), and
49
+ * • `rastack ci` — the deploy pipeline (compile the manifest the Rust backend
50
+ * reads + the OpenAPI contract, then build/ship).
51
+ */
52
+ function runCompile(resourcesDir, outDir) {
53
+ try {
54
+ const result = (0, index_1.compile)(resourcesDir, outDir);
55
+ for (const d of result.diagnostics)
56
+ console.warn(` ⚠ ${d.message}`);
57
+ return fs.existsSync(path.join(outDir, "schema.rastack.json"));
58
+ }
59
+ catch (err) {
60
+ console.error(` ✗ ${err.message}`);
61
+ return false;
62
+ }
63
+ }
@@ -20,10 +20,17 @@ export type CiPhase = "check" | "build" | "deploy";
20
20
  export interface CiStep {
21
21
  /** Human label, echoed before the step runs. */
22
22
  name: string;
23
- /** The shell command to execute. */
23
+ /** The shell command to execute (or, for an in-process step, a description). */
24
24
  run: string;
25
25
  /** Working directory relative to the repo root (default: repo root). */
26
26
  cwd?: string;
27
+ /**
28
+ * When set, the step is executed **in-process by the CLI** rather than through
29
+ * a shell — `run` is then a human-readable description, not a command. Today
30
+ * the only in-process action is `"compile"`, which the CLI runs via the shared
31
+ * compiler (`runCompile`) instead of shelling out to a standalone command.
32
+ */
33
+ action?: "compile";
27
34
  }
28
35
  export interface CiPlanOptions {
29
36
  /** How to invoke the rastack CLI for nested steps (default: `rastack`). */
package/dist/deploy/ci.js CHANGED
@@ -21,6 +21,19 @@ exports.checkSteps = checkSteps;
21
21
  exports.buildSteps = buildSteps;
22
22
  exports.deploySteps = deploySteps;
23
23
  exports.ciPlan = ciPlan;
24
+ /**
25
+ * The in-process compile step shared by `check` and `build`: it produces
26
+ * `schema.rastack.json` + `openapi.json` in `cfg.outDir` without a subprocess,
27
+ * so compilation lives inside the two modalities of use (`dev` and `ci`) rather
28
+ * than in a separately-invoked `compile` command.
29
+ */
30
+ function compileStep(cfg) {
31
+ return {
32
+ name: "Compile manifest + OpenAPI",
33
+ run: `compile ${cfg.resourcesDir} ${cfg.outDir} (in-process)`,
34
+ action: "compile",
35
+ };
36
+ }
24
37
  /**
25
38
  * A stack Output value: the literal from `cfg.outputs` if wired, else a shell
26
39
  * command substitution that resolves it live from CloudFormation. Keeping the
@@ -46,10 +59,7 @@ function checkSteps(cfg, bin) {
46
59
  name: "Validate resource graph",
47
60
  run: `${bin} check ${cfg.resourcesDir}`,
48
61
  },
49
- {
50
- name: "Compile manifest + OpenAPI",
51
- run: `${bin} compile ${cfg.resourcesDir} ${cfg.outDir}`,
52
- },
62
+ compileStep(cfg),
53
63
  ];
54
64
  }
55
65
  /**
@@ -58,15 +68,11 @@ function checkSteps(cfg, bin) {
58
68
  * binary together with the files it reads at `/var/task`.
59
69
  */
60
70
  function buildSteps(cfg, opts = {}) {
61
- const bin = opts.bin ?? "rastack";
62
71
  const stage = stageDir(cfg);
63
72
  const archFlag = cfg.lambda.arch === "arm64" ? "--arm64" : "--x86-64";
64
73
  const jwks = stackOutput(cfg, "JwksUrl", opts.jwksUrl ?? cfg.outputs?.jwksUrl);
65
74
  return [
66
- {
67
- name: "Compile manifest + OpenAPI",
68
- run: `${bin} compile ${cfg.resourcesDir} ${cfg.outDir}`,
69
- },
75
+ compileStep(cfg),
70
76
  {
71
77
  name: "Cross-build the Lambda binary",
72
78
  run: `cargo lambda build --release ${archFlag} --bin ${cfg.lambda.bin} --features ${cfg.lambda.features}`,
@@ -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,16 +57,9 @@ const child_process_1 = require("child_process");
57
57
  const fs = __importStar(require("fs"));
58
58
  const http = __importStar(require("http"));
59
59
  const path = __importStar(require("path"));
60
+ const compile_1 = require("./compile");
60
61
  const harness_1 = require("./dev/harness");
61
62
  const HERE = __dirname; // dist/ when installed — the bundle lives beside us.
62
- /** Compile the resources to schema.rastack.json + openapi.json in outDir. */
63
- function compile(resourcesDir, outDir) {
64
- const compiler = path.join(HERE, "rastack-compile.js");
65
- const result = (0, child_process_1.spawnSync)(process.execPath, [compiler, "compile", resourcesDir, outDir], { stdio: "inherit" });
66
- if (result.status !== 0)
67
- return false;
68
- return fs.existsSync(path.join(outDir, "schema.rastack.json"));
69
- }
70
63
  /** Locate the prebuilt wasm bundle dir shipped in the package (no cargo fallback). */
71
64
  function resolveWasmDir() {
72
65
  for (const dir of (0, harness_1.wasmBundleCandidates)(HERE)) {
@@ -121,7 +114,7 @@ function main(argv) {
121
114
  const args = (0, harness_1.parseAdminArgs)(argv);
122
115
  console.log(`\n rastack admin — the database console, built with React API Stack\n`);
123
116
  console.log(`▸ Compiling ${args.resourcesDir} → ${args.outDir}`);
124
- if (!compile(args.resourcesDir, args.outDir)) {
117
+ if (!(0, compile_1.runCompile)(args.resourcesDir, args.outDir)) {
125
118
  console.error(" ✗ compile failed — fix the resource graph and re-run.");
126
119
  process.exit(1);
127
120
  }
@@ -18,6 +18,7 @@
18
18
  */
19
19
  Object.defineProperty(exports, "__esModule", { value: true });
20
20
  const child_process_1 = require("child_process");
21
+ const compile_1 = require("./compile");
21
22
  const ci_1 = require("./deploy/ci");
22
23
  const io_1 = require("./deploy/io");
23
24
  const PHASES = ["check", "build", "deploy"];
@@ -74,12 +75,23 @@ function printPlan(phase, steps) {
74
75
  steps.forEach((s, i) => {
75
76
  const at = s.cwd ? ` (in ${s.cwd}/)` : "";
76
77
  console.log(` ${i + 1}. ${s.name}${at}`);
77
- console.log(` $ ${s.run}\n`);
78
+ // In-process steps have no shell command; `run` is a description.
79
+ console.log(` ${s.action ? "» " : "$ "}${s.run}\n`);
78
80
  });
79
81
  }
80
- function runSteps(phase, steps) {
82
+ function runSteps(phase, steps, cfg) {
81
83
  for (const step of steps) {
82
84
  console.log(`\n▸ ${step.name}${step.cwd ? ` (in ${step.cwd}/)` : ""}`);
85
+ if (step.action === "compile") {
86
+ // Compilation runs in-process — it lives inside `ci` (and `dev`), not in a
87
+ // separately-invoked `compile` command.
88
+ console.log(` » ${step.run}`);
89
+ if (!(0, compile_1.runCompile)(cfg.resourcesDir, cfg.outDir)) {
90
+ console.error(`\n ✗ step failed: ${step.name}`);
91
+ process.exit(1);
92
+ }
93
+ continue;
94
+ }
83
95
  console.log(` $ ${step.run}`);
84
96
  try {
85
97
  // Runs a CI pipeline step derived from the developer's own deploy config
@@ -111,7 +123,7 @@ function main(argv) {
111
123
  if (plan)
112
124
  printPlan(phase, steps);
113
125
  else
114
- runSteps(phase, steps);
126
+ runSteps(phase, steps, cfg);
115
127
  }
116
128
  if (require.main === module) {
117
129
  main(process.argv.slice(2));
@@ -1,11 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * `rastack compile | check | list | urls` — the TypeScript-first replacement for the
4
- * old Django `manage.py rastack` command.
3
+ * `rastack check | list | urls` — introspection over the TypeScript-first
4
+ * resource graph.
5
5
  *
6
- * rastack compile [resourcesDir] [outDir] defs → schema.rastack.json + openapi.json
7
6
  * rastack check [resourcesDir] validate the resource graph (exit != 0 on error)
8
7
  * rastack list [resourcesDir] print every resource + its fields/relations
9
8
  * rastack urls [resourcesDir] print the generated /api/{app}/v1/{model}/ routes
9
+ *
10
+ * Compilation itself (defs → schema.rastack.json + openapi.json) is no longer a
11
+ * command here: it runs in-process inside the two modalities of use — `rastack
12
+ * dev`/`admin` (local) and `rastack ci` (deploy) — via the shared `runCompile`.
10
13
  */
11
14
  export {};
@@ -1,30 +1,23 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
3
  /**
4
- * `rastack compile | check | list | urls` — the TypeScript-first replacement for the
5
- * old Django `manage.py rastack` command.
4
+ * `rastack check | list | urls` — introspection over the TypeScript-first
5
+ * resource graph.
6
6
  *
7
- * rastack compile [resourcesDir] [outDir] defs → schema.rastack.json + openapi.json
8
7
  * rastack check [resourcesDir] validate the resource graph (exit != 0 on error)
9
8
  * rastack list [resourcesDir] print every resource + its fields/relations
10
9
  * rastack urls [resourcesDir] print the generated /api/{app}/v1/{model}/ routes
10
+ *
11
+ * Compilation itself (defs → schema.rastack.json + openapi.json) is no longer a
12
+ * command here: it runs in-process inside the two modalities of use — `rastack
13
+ * dev`/`admin` (local) and `rastack ci` (deploy) — via the shared `runCompile`.
11
14
  */
12
15
  Object.defineProperty(exports, "__esModule", { value: true });
13
16
  const compile_1 = require("./compile");
14
17
  const DEFAULT_RESOURCES = "resources";
15
- const DEFAULT_OUT = ".rastack";
16
18
  function main(argv) {
17
19
  const [sub, ...rest] = argv;
18
20
  switch (sub) {
19
- case "compile": {
20
- const resourcesDir = rest[0] || DEFAULT_RESOURCES;
21
- const outDir = rest[1] || DEFAULT_OUT;
22
- const result = (0, compile_1.compile)(resourcesDir, outDir);
23
- for (const d of result.diagnostics)
24
- console.warn(` ⚠ ${d.message}`);
25
- console.log(`✓ compiled ${result.manifest.resources.length} resource(s) → ${result.files.join(", ")}`);
26
- break;
27
- }
28
21
  case "check": {
29
22
  const { resources, diagnostics } = (0, compile_1.analyzeProject)(rest[0] || DEFAULT_RESOURCES);
30
23
  const errors = [
@@ -54,7 +47,9 @@ function main(argv) {
54
47
  for (const r of resources) {
55
48
  console.log(`${r.app}.${r.model}`);
56
49
  for (const f of r.fields) {
57
- const rel = f.relation ? ` → ${f.relation.app}.${f.relation.model}` : "";
50
+ const rel = f.relation
51
+ ? ` → ${f.relation.app}.${f.relation.model}`
52
+ : "";
58
53
  console.log(` ${f.name}: ${f.type}${rel}`);
59
54
  }
60
55
  if (r.sync) {
@@ -78,9 +73,8 @@ function main(argv) {
78
73
  break;
79
74
  }
80
75
  default:
81
- console.error(`Unknown compile subcommand: "${sub}"\n\n` +
76
+ console.error(`Unknown subcommand: "${sub}"\n\n` +
82
77
  `Usage:\n` +
83
- ` rastack compile [resourcesDir] [outDir]\n` +
84
78
  ` rastack check [resourcesDir]\n` +
85
79
  ` rastack list [resourcesDir]\n` +
86
80
  ` rastack urls [resourcesDir]\n`);
@@ -57,21 +57,15 @@ 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"));
62
+ const compile_1 = require("./compile");
61
63
  const harness_1 = require("./dev/harness");
62
64
  const HERE = __dirname; // dist/ when installed
63
- /** Compile the resources to schema.rastack.json + openapi.json in outDir. */
64
- function compile(resourcesDir, outDir) {
65
- const compiler = path.join(HERE, "rastack-compile.js");
66
- const result = (0, child_process_1.spawnSync)(process.execPath, [compiler, "compile", resourcesDir, outDir], { stdio: "inherit" });
67
- if (result.status !== 0)
68
- return false;
69
- return fs.existsSync(path.join(outDir, "schema.rastack.json"));
70
- }
71
65
  /** True when `cargo` is on PATH (the from-source wasm fallback needs it). */
72
66
  function cargoAvailable() {
73
67
  try {
74
- return ((0, child_process_1.spawnSync)("cargo", ["--version"], { stdio: "ignore" }).status === 0);
68
+ return (0, child_process_1.spawnSync)("cargo", ["--version"], { stdio: "ignore" }).status === 0;
75
69
  }
76
70
  catch {
77
71
  return false;
@@ -151,14 +145,198 @@ function openBrowser(url) {
151
145
  /* non-fatal — the URL is printed anyway */
152
146
  }
153
147
  }
154
- 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) {
155
326
  const args = (0, harness_1.parseDevArgs)(argv);
327
+ const cwd = process.cwd();
156
328
  console.log(`\n rastack dev — your app, in your browser over WASM\n`);
157
329
  console.log(`▸ Compiling ${args.resourcesDir} → ${args.outDir}`);
158
- if (!compile(args.resourcesDir, args.outDir)) {
330
+ if (!(0, compile_1.runCompile)(args.resourcesDir, args.outDir)) {
159
331
  console.error(" ✗ compile failed — fix the resource graph and re-run.");
160
332
  process.exit(1);
161
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
+ }
162
340
  const wasmDir = resolveWasmDir();
163
341
  if (!wasmDir) {
164
342
  console.error("\n ✗ No WASM engine available.\n" +
@@ -168,8 +346,6 @@ function main(argv) {
168
346
  " cargo install wasm-bindgen-cli\n");
169
347
  process.exit(1);
170
348
  }
171
- // Serve the developer's own app when there is one; otherwise the welcome page.
172
- const appRoot = resolveAppRoot(args.appDir);
173
349
  const server = http.createServer((req, res) => {
174
350
  const url = (req.url || "/").split("?")[0];
175
351
  if (req.method !== "GET") {
@@ -220,5 +396,8 @@ function main(argv) {
220
396
  });
221
397
  }
222
398
  if (require.main === module) {
223
- main(process.argv.slice(2));
399
+ main(process.argv.slice(2)).catch((err) => {
400
+ console.error(err);
401
+ process.exit(1);
402
+ });
224
403
  }
package/dist/rastack.d.ts CHANGED
@@ -2,8 +2,10 @@
2
2
  /**
3
3
  * rastack — unified CLI for React API Stack
4
4
  *
5
+ * Compilation is not a command of its own: it runs in-process inside the two
6
+ * modalities of use — `rastack dev`/`admin` (local) and `rastack ci` (deploy).
7
+ *
5
8
  * Commands:
6
- * rastack compile [resourcesDir] [outDir] TypeScript resources → schema.rastack.json + openapi.json
7
9
  * rastack check [resourcesDir] Validate the resource graph
8
10
  * rastack list [resourcesDir] Print resources, fields and relations
9
11
  * rastack urls [resourcesDir] Print the generated /api/{app}/v1/{model}/ routes