rastack 0.0.55 → 0.0.57

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.
@@ -1,18 +1,22 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
3
  /**
4
- * `rastack dev` — local development, always over the WASM engine.
4
+ * `rastack dev` — local development of **your app**, always over the WASM engine.
5
5
  *
6
- * rastack dev [resourcesDir] [--port <n>] [--warehouse <dir>] [--out <dir>] [--no-open]
6
+ * rastack dev [resourcesDir] [--port <n>] [--warehouse <dir>] [--out <dir>] [--app <dir>] [--no-open]
7
7
  *
8
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 app +
10
- * admin locally over WebAssembly — no native server, no cargo:
11
- * • the bundled React admin (`/dev/admin.js`),
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:
11
+ * • the developer's app (`index.html` from the resolved app root), or a
12
+ * Django-style welcome page when the project has no app yet,
12
13
  * • the prebuilt WASM engine (`/wasm/…`) shipped in the package,
13
14
  * • the compiled `schema.rastack.json` / `openapi.json`,
14
15
  * • the committed Iceberg warehouse (`/data/warehouse/…`) to seed from.
15
16
  *
17
+ * It does **not** serve the admin console — that is a separate command,
18
+ * `rastack admin`.
19
+ *
16
20
  * The engine is resolved from the package's prebuilt bundle; if none is present
17
21
  * but a Rust + wasm toolchain is, it is built from source as a fallback.
18
22
  */
@@ -54,25 +58,26 @@ const child_process_1 = require("child_process");
54
58
  const fs = __importStar(require("fs"));
55
59
  const http = __importStar(require("http"));
56
60
  const path = __importStar(require("path"));
61
+ const compile_1 = require("./compile");
57
62
  const harness_1 = require("./dev/harness");
58
63
  const HERE = __dirname; // dist/ when installed
59
- /** Compile the resources to schema.rastack.json + openapi.json in outDir. */
60
- function compile(resourcesDir, outDir) {
61
- const compiler = path.join(HERE, "rastack-compile.js");
62
- const result = (0, child_process_1.spawnSync)(process.execPath, [compiler, "compile", resourcesDir, outDir], { stdio: "inherit" });
63
- if (result.status !== 0)
64
- return false;
65
- return fs.existsSync(path.join(outDir, "schema.rastack.json"));
66
- }
67
64
  /** True when `cargo` is on PATH (the from-source wasm fallback needs it). */
68
65
  function cargoAvailable() {
69
66
  try {
70
- return ((0, child_process_1.spawnSync)("cargo", ["--version"], { stdio: "ignore" }).status === 0);
67
+ return (0, child_process_1.spawnSync)("cargo", ["--version"], { stdio: "ignore" }).status === 0;
71
68
  }
72
69
  catch {
73
70
  return false;
74
71
  }
75
72
  }
73
+ /** First app-root candidate that holds an `index.html`, or null (→ welcome). */
74
+ function resolveAppRoot(appDir) {
75
+ for (const dir of (0, harness_1.appRootCandidates)(appDir)) {
76
+ if (fs.existsSync(path.join(dir, harness_1.APP_ENTRY)))
77
+ return dir;
78
+ }
79
+ return null;
80
+ }
76
81
  /** Locate the prebuilt wasm bundle dir, building from source as a fallback. */
77
82
  function resolveWasmDir() {
78
83
  for (const dir of (0, harness_1.wasmBundleCandidates)(HERE)) {
@@ -141,9 +146,9 @@ function openBrowser(url) {
141
146
  }
142
147
  function main(argv) {
143
148
  const args = (0, harness_1.parseDevArgs)(argv);
144
- console.log(`\n rastack dev — the app + admin, in your browser over WASM\n`);
149
+ console.log(`\n rastack dev — your app, in your browser over WASM\n`);
145
150
  console.log(`▸ Compiling ${args.resourcesDir} → ${args.outDir}`);
146
- if (!compile(args.resourcesDir, args.outDir)) {
151
+ if (!(0, compile_1.runCompile)(args.resourcesDir, args.outDir)) {
147
152
  console.error(" ✗ compile failed — fix the resource graph and re-run.");
148
153
  process.exit(1);
149
154
  }
@@ -156,38 +161,53 @@ function main(argv) {
156
161
  " cargo install wasm-bindgen-cli\n");
157
162
  process.exit(1);
158
163
  }
159
- const adminBundle = path.join(HERE, "admin.js");
160
- if (!fs.existsSync(adminBundle)) {
161
- console.error(" ✗ admin bundle (dist/admin.js) not found — reinstall rastack.");
162
- process.exit(1);
163
- }
164
+ // Serve the developer's own app when there is one; otherwise the welcome page.
165
+ const appRoot = resolveAppRoot(args.appDir);
164
166
  const server = http.createServer((req, res) => {
165
167
  const url = (req.url || "/").split("?")[0];
166
168
  if (req.method !== "GET") {
167
169
  res.writeHead(405).end("Method not allowed");
168
170
  return;
169
171
  }
170
- if (url === "/" || url === "/index.html") {
171
- res
172
- .writeHead(200, { "Content-Type": "text/html; charset=utf-8" })
173
- .end((0, harness_1.renderShell)());
174
- return;
175
- }
176
- if (url === "/dev/admin.js")
177
- return serveFile(res, adminBundle);
172
+ // Reserved backend routes always win over the app's static files.
178
173
  if (url.startsWith("/wasm/"))
179
174
  return serveUnder(res, wasmDir, url.slice("/wasm/".length));
180
175
  if (url === "/schema.rastack.json" || url === "/openapi.json")
181
176
  return serveUnder(res, args.outDir, path.basename(url));
182
177
  if (url.startsWith("/data/warehouse/"))
183
178
  return serveUnder(res, args.warehouseDir, url.slice("/data/warehouse/".length));
184
- res.writeHead(404).end("Not found");
179
+ // No app → the Django-style welcome page (root only; else 404).
180
+ if (!appRoot) {
181
+ if (url === "/" || url === "/index.html") {
182
+ res
183
+ .writeHead(200, { "Content-Type": "text/html; charset=utf-8" })
184
+ .end((0, harness_1.renderWelcome)());
185
+ return;
186
+ }
187
+ res.writeHead(404).end("Not found");
188
+ return;
189
+ }
190
+ // Serve the app: its index.html at `/`, an existing file as-is, and any
191
+ // other route back to index.html (SPA client-side routing).
192
+ if (url === "/" || url === "/index.html")
193
+ return serveFile(res, path.join(appRoot, harness_1.APP_ENTRY));
194
+ const rel = decodeURIComponent(url).replace(/^\/+/, "");
195
+ const target = path.resolve(appRoot, rel);
196
+ const safeRoot = path.resolve(appRoot);
197
+ const inRoot = target === safeRoot || target.startsWith(safeRoot + path.sep);
198
+ if (inRoot && fs.existsSync(target) && fs.statSync(target).isFile())
199
+ return serveFile(res, target);
200
+ return serveFile(res, path.join(appRoot, harness_1.APP_ENTRY));
185
201
  });
186
202
  server.listen(args.port, () => {
187
203
  const url = `http://localhost:${args.port}`;
188
204
  console.log(` ✓ WASM engine: ${path.relative(process.cwd(), wasmDir)}`);
189
- console.log(`\n ▸ ${url} (admin at ${url}/#/)\n`);
190
- console.log(" Press Ctrl+C to stop.\n");
205
+ if (appRoot)
206
+ console.log(` ✓ app: ${path.relative(process.cwd(), appRoot) || "."}`);
207
+ else
208
+ console.log(` ○ no app found — serving the welcome page`);
209
+ console.log(`\n ▸ ${url}\n`);
210
+ console.log(" Need the database console? Run `rastack admin`. Ctrl+C to stop.\n");
191
211
  if (args.open)
192
212
  openBrowser(url);
193
213
  });
@@ -82,10 +82,17 @@ function readVersion(file) {
82
82
  * sanitized (`sanitizeVersion`). POSIX keeps the shell-free `execFile`.
83
83
  */
84
84
  function runNpm(args, opts) {
85
- // nosemgrep: javascript.lang.security.detect-child-process.detect-child-process
86
- return NPM_SHELL
87
- ? (0, child_process_1.execSync)((0, update_1.npmShellCommand)(NPM, args), opts)
88
- : (0, child_process_1.execFileSync)(NPM, args, opts);
85
+ if (NPM_SHELL) {
86
+ // Windows: launch the `npm.cmd` shim through a shell (a bare `.cmd` spawn is
87
+ // rejected with EINVAL since the Node CVE-2024-27980 fix). The only
88
+ // non-literal token — the version — is sanitized in `parseUpdateFlags`, and
89
+ // every other arg is a fixed literal, so nothing injectable reaches it. The
90
+ // nosemgrep must sit on the line directly above the `execSync` call for the
91
+ // engine to honour it.
92
+ // nosemgrep: javascript.lang.security.detect-child-process.detect-child-process
93
+ return (0, child_process_1.execSync)((0, update_1.npmShellCommand)(NPM, args), opts);
94
+ }
95
+ return (0, child_process_1.execFileSync)(NPM, args, opts);
89
96
  }
90
97
  /** Capture `npm <args>` stdout, or null when npm is absent / the call fails. */
91
98
  function npmCapture(args) {
package/dist/rastack.d.ts CHANGED
@@ -2,15 +2,17 @@
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
10
12
  * rastack generate [--schema-only] Generate typed hooks from OpenAPI
11
13
  * rastack tokens [input] [outDir] Compile design tokens → CSS vars + typed theme
12
14
  * rastack design [input] [--port n] Live design-system studio (showcase + edit)
13
- * rastack dev [--port n] [--warehouse d] Local dev: run the app + admin in-browser over WASM
15
+ * rastack dev [--port n] [--warehouse d] Local dev: run your app in-browser over WASM
14
16
  * rastack admin [--warehouse dir] [--port n] Full admin console — React UI (rastack/components) over the native server
15
17
  * rastack import <file> --resource app.model Load CSV/XLSX/PDF data and validate it (state machine)
16
18
  * rastack scan [files...] [--fail-on-pii] Scan CSVs for PII and schema info
package/dist/rastack.js CHANGED
@@ -3,15 +3,17 @@
3
3
  /**
4
4
  * rastack — unified CLI for React API Stack
5
5
  *
6
+ * Compilation is not a command of its own: it runs in-process inside the two
7
+ * modalities of use — `rastack dev`/`admin` (local) and `rastack ci` (deploy).
8
+ *
6
9
  * Commands:
7
- * rastack compile [resourcesDir] [outDir] TypeScript resources → schema.rastack.json + openapi.json
8
10
  * rastack check [resourcesDir] Validate the resource graph
9
11
  * rastack list [resourcesDir] Print resources, fields and relations
10
12
  * rastack urls [resourcesDir] Print the generated /api/{app}/v1/{model}/ routes
11
13
  * rastack generate [--schema-only] Generate typed hooks from OpenAPI
12
14
  * rastack tokens [input] [outDir] Compile design tokens → CSS vars + typed theme
13
15
  * rastack design [input] [--port n] Live design-system studio (showcase + edit)
14
- * rastack dev [--port n] [--warehouse d] Local dev: run the app + admin in-browser over WASM
16
+ * rastack dev [--port n] [--warehouse d] Local dev: run your app in-browser over WASM
15
17
  * rastack admin [--warehouse dir] [--port n] Full admin console — React UI (rastack/components) over the native server
16
18
  * rastack import <file> --resource app.model Load CSV/XLSX/PDF data and validate it (state machine)
17
19
  * rastack scan [files...] [--fail-on-pii] Scan CSVs for PII and schema info
@@ -64,6 +66,15 @@ switch (command) {
64
66
  printVersion();
65
67
  break;
66
68
  case "compile":
69
+ // Compilation is no longer a standalone command — it runs in-process as
70
+ // part of the two modalities of use. Point the way rather than 404.
71
+ console.error("`rastack compile` has been removed — compilation now runs automatically\n" +
72
+ "inside the two modalities of use:\n" +
73
+ " • rastack dev local development (compiles, then serves over WASM)\n" +
74
+ " • rastack ci the deploy pipeline (compiles the manifest + OpenAPI)\n\n" +
75
+ "Introspect the resource graph with: rastack check | list | urls\n");
76
+ process.exit(1);
77
+ break;
67
78
  case "check":
68
79
  case "list":
69
80
  case "urls":
@@ -71,8 +82,9 @@ switch (command) {
71
82
  run("rastack-compile.js", [command, ...rest]);
72
83
  break;
73
84
  case "dev":
74
- // Local development: serve the app + admin in the browser over the WASM
75
- // engine (no native server, no cargo). Replaces `serve` + `wasm`.
85
+ // Local development: serve your app in the browser over the WASM engine
86
+ // (no native server, no cargo). The admin console is a separate command,
87
+ // `rastack admin`. Replaces `serve` + `wasm`.
76
88
  run("rastack-dev.js", rest);
77
89
  break;
78
90
  case "admin":
@@ -132,12 +144,11 @@ switch (command) {
132
144
  default:
133
145
  console.error(`Unknown command: "${command}"\n\n` +
134
146
  `Usage:\n` +
135
- ` rastack compile [resourcesDir] [outDir]\n` +
136
147
  ` rastack check | list | urls [resourcesDir]\n` +
137
148
  ` rastack generate [--schema-only]\n` +
138
149
  ` rastack tokens [input] [outDir] [--prefix p] Compile design tokens → CSS vars + typed theme\n` +
139
150
  ` rastack design [input] [--port n] Live design-system studio (showcase + edit)\n` +
140
- ` rastack dev [--port n] [--warehouse dir] [--no-open] Run the app + admin in-browser over WASM (local dev)\n` +
151
+ ` rastack dev [--port n] [--warehouse dir] [--app dir] [--no-open] Run your app in-browser over WASM (local dev)\n` +
141
152
  ` rastack admin [--warehouse dir] [--port n] [--no-open] Full admin console (React UI over the native server; needs a built rastack-server)\n` +
142
153
  ` rastack import <file.csv|.xlsx|.pdf> --resource app.model [--related app.model=<file>] [--out cleaned.json]\n` +
143
154
  ` rastack scan [files...] [--fail-on-pii]\n` +
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rastack",
3
- "version": "0.0.55",
3
+ "version": "0.0.57",
4
4
  "description": "",
5
5
  "main": "runtime.ts",
6
6
  "types": "runtime.ts",
@@ -21,7 +21,7 @@
21
21
  "author": "",
22
22
  "license": "ISC",
23
23
  "dependencies": {
24
- "@apidevtools/json-schema-ref-parser": "^15.4.0",
24
+ "@apidevtools/json-schema-ref-parser": "^15.5.0",
25
25
  "@tanstack/react-query": "^5.101.2",
26
26
  "@ts-stack/openapi-spec": "^3.1.5",
27
27
  "@types/json-schema": "^7.0.15",
@@ -37,9 +37,9 @@
37
37
  "lodash": "^4.18.0",
38
38
  "luxon": "^3.7.2",
39
39
  "minimatch": "^10.2.5",
40
- "prettier": "^3.9.4",
40
+ "prettier": "^3.9.5",
41
41
  "react-query": "^3.39.3",
42
- "typescript": "^6.0.3",
42
+ "typescript": "~5.9.3",
43
43
  "usehooks-ts": "^3.1.1"
44
44
  },
45
45
  "peerDependencies": {
@@ -49,10 +49,10 @@
49
49
  "@types/jest": "^30.0.0",
50
50
  "@types/js-yaml": "^4.0.9",
51
51
  "@types/mocha": "^10.0.6",
52
- "@types/node": "^26.1.0",
52
+ "@types/node": "^26.1.1",
53
53
  "@types/react-dom": "^19.2.0",
54
- "esbuild": "^0.25.0",
55
- "eslint": "^10.6.0",
54
+ "esbuild": "^0.28.1",
55
+ "eslint": "^10.7.0",
56
56
  "jest": "^30.4.2",
57
57
  "react": "^19.2.0",
58
58
  "react-dom": "^19.2.0",
@@ -21,6 +21,7 @@ export type {
21
21
  EntityTypeExport,
22
22
  } from "./entities";
23
23
  export { buildManifest } from "./manifest";
24
+ export { runCompile } from "./run";
24
25
  export { buildOpenApi, writeSchema } from "./openapi";
25
26
  export * from "./model";
26
27
 
@@ -60,7 +61,10 @@ export function resolveResourceFiles(input: string): string[] {
60
61
  for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
61
62
  if (entry.isDirectory()) {
62
63
  if (!IGNORE_DIRS.has(entry.name)) walk(path.join(dir, entry.name));
63
- } else if (/\.tsx?$/.test(entry.name) && !entry.name.endsWith(".d.ts")) {
64
+ } else if (
65
+ /\.tsx?$/.test(entry.name) &&
66
+ !entry.name.endsWith(".d.ts")
67
+ ) {
64
68
  files.push(path.resolve(dir, entry.name));
65
69
  }
66
70
  }
@@ -96,10 +100,14 @@ export function analyzeProject(input: string): ProjectAnalysis {
96
100
  const defined = new Set(fromDsl.resources.map((r) => `${r.app}.${r.model}`));
97
101
  const resources = [
98
102
  ...fromDsl.resources,
99
- ...fromInterfaces.resources.filter((r) => !defined.has(`${r.app}.${r.model}`)),
103
+ ...fromInterfaces.resources.filter(
104
+ (r) => !defined.has(`${r.app}.${r.model}`),
105
+ ),
100
106
  ];
101
107
  resources.sort((a, b) =>
102
- a.app === b.app ? a.model.localeCompare(b.model) : a.app.localeCompare(b.app),
108
+ a.app === b.app
109
+ ? a.model.localeCompare(b.model)
110
+ : a.app.localeCompare(b.app),
103
111
  );
104
112
 
105
113
  // Registry entries follow the same merge rule as the resources: the DSL
@@ -237,7 +245,8 @@ export function collisions(resources: ResourceModel[]): string[] {
237
245
  }
238
246
  }
239
247
  for (const [key, count] of seen) {
240
- if (count > 1) errors.push(`Duplicate resource ${key} defined ${count} times.`);
248
+ if (count > 1)
249
+ errors.push(`Duplicate resource ${key} defined ${count} times.`);
241
250
  }
242
251
  return errors;
243
252
  }
@@ -0,0 +1,26 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import { compile } from "./index";
4
+
5
+ /**
6
+ * Run the compiler **in-process**, writing `schema.rastack.json` +
7
+ * `openapi.json` (and `rastack-env.d.ts`) into `outDir`. Prints any warnings
8
+ * and, on failure, the error; returns `true` on success.
9
+ *
10
+ * This is the single entry point the two real modalities of use share instead
11
+ * of shelling out to a standalone `compile` command:
12
+ * • `rastack dev` / `rastack admin` — local development (compile, then serve
13
+ * the app + admin in the browser over WASM), and
14
+ * • `rastack ci` — the deploy pipeline (compile the manifest the Rust backend
15
+ * reads + the OpenAPI contract, then build/ship).
16
+ */
17
+ export function runCompile(resourcesDir: string, outDir: string): boolean {
18
+ try {
19
+ const result = compile(resourcesDir, outDir);
20
+ for (const d of result.diagnostics) console.warn(` ⚠ ${d.message}`);
21
+ return fs.existsSync(path.join(outDir, "schema.rastack.json"));
22
+ } catch (err) {
23
+ console.error(` ✗ ${(err as Error).message}`);
24
+ return false;
25
+ }
26
+ }
package/src/deploy/ci.ts CHANGED
@@ -23,10 +23,31 @@ export type CiPhase = "check" | "build" | "deploy";
23
23
  export interface CiStep {
24
24
  /** Human label, echoed before the step runs. */
25
25
  name: string;
26
- /** The shell command to execute. */
26
+ /** The shell command to execute (or, for an in-process step, a description). */
27
27
  run: string;
28
28
  /** Working directory relative to the repo root (default: repo root). */
29
29
  cwd?: string;
30
+ /**
31
+ * When set, the step is executed **in-process by the CLI** rather than through
32
+ * a shell — `run` is then a human-readable description, not a command. Today
33
+ * the only in-process action is `"compile"`, which the CLI runs via the shared
34
+ * compiler (`runCompile`) instead of shelling out to a standalone command.
35
+ */
36
+ action?: "compile";
37
+ }
38
+
39
+ /**
40
+ * The in-process compile step shared by `check` and `build`: it produces
41
+ * `schema.rastack.json` + `openapi.json` in `cfg.outDir` without a subprocess,
42
+ * so compilation lives inside the two modalities of use (`dev` and `ci`) rather
43
+ * than in a separately-invoked `compile` command.
44
+ */
45
+ function compileStep(cfg: DeployConfig): CiStep {
46
+ return {
47
+ name: "Compile manifest + OpenAPI",
48
+ run: `compile ${cfg.resourcesDir} ${cfg.outDir} (in-process)`,
49
+ action: "compile",
50
+ };
30
51
  }
31
52
 
32
53
  export interface CiPlanOptions {
@@ -72,10 +93,7 @@ export function checkSteps(cfg: DeployConfig, bin: string): CiStep[] {
72
93
  name: "Validate resource graph",
73
94
  run: `${bin} check ${cfg.resourcesDir}`,
74
95
  },
75
- {
76
- name: "Compile manifest + OpenAPI",
77
- run: `${bin} compile ${cfg.resourcesDir} ${cfg.outDir}`,
78
- },
96
+ compileStep(cfg),
79
97
  ];
80
98
  }
81
99
 
@@ -88,7 +106,6 @@ export function buildSteps(
88
106
  cfg: DeployConfig,
89
107
  opts: CiPlanOptions = {},
90
108
  ): CiStep[] {
91
- const bin = opts.bin ?? "rastack";
92
109
  const stage = stageDir(cfg);
93
110
  const archFlag = cfg.lambda.arch === "arm64" ? "--arm64" : "--x86-64";
94
111
  const jwks = stackOutput(
@@ -97,10 +114,7 @@ export function buildSteps(
97
114
  opts.jwksUrl ?? cfg.outputs?.jwksUrl,
98
115
  );
99
116
  return [
100
- {
101
- name: "Compile manifest + OpenAPI",
102
- run: `${bin} compile ${cfg.resourcesDir} ${cfg.outDir}`,
103
- },
117
+ compileStep(cfg),
104
118
  {
105
119
  name: "Cross-build the Lambda binary",
106
120
  run: `cargo lambda build --release ${archFlag} --bin ${cfg.lambda.bin} --features ${cfg.lambda.features}`,
@@ -1,16 +1,20 @@
1
1
  /**
2
2
  * Pure core of `rastack dev` — the local, always-WASM development server.
3
3
  *
4
- * `rastack dev` boots the app + admin **in the browser** over the WebAssembly
5
- * engine (the same app-generic `rastack-api-core` the Lambda runs, fed the app's
4
+ * `rastack dev` runs **your app** in the browser over the WebAssembly engine
5
+ * (the same app-generic `rastack-api-core` the Lambda runs, fed the app's
6
6
  * compiled manifest). No native server, no cargo: a dependency-free static
7
- * server hands the browser the prebuilt wasm engine, the compiled schema, and
8
- * the committed Iceberg warehouse, and a bundled React admin drives it.
7
+ * server hands the browser the prebuilt wasm engine, the compiled schema, the
8
+ * committed Iceberg warehouse, and the developer's own app. It does **not**
9
+ * serve the admin console — that is a separate command, `rastack admin`.
10
+ *
11
+ * When the project has no app to serve, `dev` shows a Django-style welcome
12
+ * page (`renderWelcome`) so a fresh scaffold still boots to something friendly.
9
13
  *
10
14
  * Everything decidable without IO lives here (arg parsing, MIME types, locating
11
- * the wasm bundle, the HTML shell) so it is unit-tested without a socket or a
12
- * filesystem; `rastack-dev.ts` is the thin IO wrapper that compiles, resolves
13
- * the engine, and serves.
15
+ * the wasm bundle, resolving the app root, the HTML pages) so it is unit-tested
16
+ * without a socket or a filesystem; `rastack-dev.ts` is the thin IO wrapper that
17
+ * compiles, resolves the engine and the app, and serves.
14
18
  */
15
19
 
16
20
  import * as path from "path";
@@ -22,6 +26,12 @@ export interface DevArgs {
22
26
  outDir: string;
23
27
  /** The committed Iceberg warehouse served to seed the browser engine. */
24
28
  warehouseDir: string;
29
+ /**
30
+ * Explicit app root to serve at `/` (its `index.html`). Empty when unset —
31
+ * the IO wrapper then probes `appRootCandidates`, falling back to the welcome
32
+ * page when none holds an `index.html`.
33
+ */
34
+ appDir: string;
25
35
  /** Port the dev server listens on. */
26
36
  port: number;
27
37
  /** Open the browser on start (false with `--no-open`). */
@@ -32,22 +42,25 @@ export const DEV_DEFAULTS: DevArgs = {
32
42
  resourcesDir: "resources",
33
43
  outDir: ".rastack",
34
44
  warehouseDir: "data/warehouse",
45
+ appDir: "",
35
46
  port: 4321,
36
47
  open: true,
37
48
  };
38
49
 
39
50
  /**
40
- * Parse `rastack dev` args: `--port`, `--warehouse`, `--out`, `--no-open`, and a
41
- * single positional resources dir. Unknown flags are ignored (forgiving CLI).
51
+ * Parse `rastack dev` args: `--port`, `--warehouse`, `--out`, `--app`,
52
+ * `--no-open`, and a single positional resources dir. Unknown flags are ignored
53
+ * (forgiving CLI).
42
54
  */
43
55
  export function parseDevArgs(argv: string[]): DevArgs {
44
56
  const positional: string[] = [];
45
- let { resourcesDir, outDir, warehouseDir, port, open } = DEV_DEFAULTS;
57
+ let { resourcesDir, outDir, warehouseDir, appDir, port, open } = DEV_DEFAULTS;
46
58
  for (let i = 0; i < argv.length; i++) {
47
59
  const a = argv[i];
48
60
  if (a === "--port") port = Number(argv[++i]) || port;
49
61
  else if (a === "--warehouse") warehouseDir = argv[++i] ?? warehouseDir;
50
62
  else if (a === "--out") outDir = argv[++i] ?? outDir;
63
+ else if (a === "--app") appDir = argv[++i] ?? appDir;
51
64
  else if (a === "--no-open") open = false;
52
65
  else if (!a.startsWith("-")) positional.push(a);
53
66
  }
@@ -55,11 +68,27 @@ export function parseDevArgs(argv: string[]): DevArgs {
55
68
  resourcesDir: positional[0] || resourcesDir,
56
69
  outDir,
57
70
  warehouseDir,
71
+ appDir,
58
72
  port,
59
73
  open,
60
74
  };
61
75
  }
62
76
 
77
+ /** The file that marks a directory as a servable app root. */
78
+ export const APP_ENTRY = "index.html";
79
+
80
+ /**
81
+ * Ordered directories to probe for the developer's app (`index.html`). An
82
+ * explicit `--app <dir>` wins outright; otherwise `dev` looks in the
83
+ * conventional static roots and finally the project root. The IO wrapper picks
84
+ * the first candidate that actually holds an `index.html`; if none does, it
85
+ * serves the welcome page instead.
86
+ */
87
+ export function appRootCandidates(appDir: string, cwd = process.cwd()): string[] {
88
+ if (appDir) return [path.resolve(cwd, appDir)];
89
+ return ["public", "www", "app", "."].map((d) => path.resolve(cwd, d));
90
+ }
91
+
63
92
  /** Args for `rastack admin` — like `DevArgs`, but bound to a real server port. */
64
93
  export interface AdminArgs {
65
94
  resourcesDir: string;
@@ -147,12 +176,14 @@ export function wasmBundleCandidates(
147
176
  export const WASM_ENTRY = "rastack_wasm.js";
148
177
 
149
178
  /**
150
- * The HTML shell served at `/`. It loads the bundled React admin
151
- * (`/dev/admin.js`), which fetches the schema/openapi, boots the wasm engine,
152
- * seeds it from `/data/warehouse/`, and renders the admin. Deliberately tiny —
153
- * all logic lives in the bundle — mirroring the `rastack design` studio.
179
+ * The HTML shell served at `/` by **`rastack admin`**. It loads the bundled
180
+ * React admin (`/dev/admin.js`), which fetches the schema/openapi, boots the
181
+ * wasm engine, seeds it from `/data/warehouse/`, and renders the admin console.
182
+ * Deliberately tiny — all logic lives in the bundle — mirroring the
183
+ * `rastack design` studio. `rastack dev` does **not** use this: it serves the
184
+ * developer's own app (or the welcome page below).
154
185
  */
155
- export function renderShell(title = "React API Stack — dev"): string {
186
+ export function renderShell(title = "React API Stack — admin"): string {
156
187
  return `<!doctype html>
157
188
  <html lang="en">
158
189
  <head>
@@ -172,3 +203,78 @@ export function renderShell(title = "React API Stack — dev"): string {
172
203
  </html>
173
204
  `;
174
205
  }
206
+
207
+ /**
208
+ * The Django-style welcome page `rastack dev` serves at `/` when the project
209
+ * has **no app** to serve (no `index.html` in any `appRootCandidates` dir).
210
+ * It confirms the dev server is up, points at the live backend endpoints the
211
+ * WASM engine exposes, and tells you how to add an app or open the admin — the
212
+ * "It worked!" moment of `django-admin runserver`, on brand for the stack.
213
+ */
214
+ export function renderWelcome(title = "React API Stack — dev"): string {
215
+ return `<!doctype html>
216
+ <html lang="en">
217
+ <head>
218
+ <meta charset="utf-8" />
219
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
220
+ <title>${title}</title>
221
+ <style>
222
+ :root { color-scheme: light dark; }
223
+ * { box-sizing: border-box; }
224
+ html, body { margin: 0; min-height: 100%; }
225
+ body {
226
+ font: 15px/1.6 system-ui, -apple-system, "Segoe UI", sans-serif;
227
+ color: #1f2328; background: #f6f8fa;
228
+ display: flex; align-items: center; justify-content: center; padding: 2rem;
229
+ }
230
+ .card {
231
+ max-width: 640px; width: 100%; background: #fff; border: 1px solid #d0d7de;
232
+ border-radius: 14px; padding: 2.5rem; box-shadow: 0 1px 3px rgba(0,0,0,.06);
233
+ }
234
+ .rocket { font-size: 2.5rem; line-height: 1; }
235
+ h1 { font-size: 1.5rem; margin: .75rem 0 .25rem; }
236
+ p.lead { color: #57606a; margin: 0 0 1.5rem; }
237
+ code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
238
+ background: #eff1f3; padding: .1em .4em; border-radius: 5px; font-size: .9em; }
239
+ ul { padding-left: 1.1rem; margin: 0; }
240
+ li { margin: .4rem 0; }
241
+ a { color: #0969da; text-decoration: none; }
242
+ a:hover { text-decoration: underline; }
243
+ .foot { margin-top: 1.75rem; padding-top: 1.25rem; border-top: 1px solid #eaeef2;
244
+ color: #6e7781; font-size: .85rem; }
245
+ @media (prefers-color-scheme: dark) {
246
+ body { color: #e6edf3; background: #0d1117; }
247
+ .card { background: #161b22; border-color: #30363d; box-shadow: none; }
248
+ p.lead { color: #8b949e; }
249
+ code { background: #21262d; }
250
+ .foot { border-color: #21262d; color: #8b949e; }
251
+ }
252
+ </style>
253
+ </head>
254
+ <body>
255
+ <div class="card">
256
+ <div class="rocket">🚀</div>
257
+ <h1>The dev server is running.</h1>
258
+ <p class="lead">
259
+ Congratulations — <strong>rastack dev</strong> is up and your API is running
260
+ in the browser over WebAssembly. You're seeing this page because there's no
261
+ app to serve yet.
262
+ </p>
263
+ <ul>
264
+ <li>Add an <code>index.html</code> (in <code>public/</code>, <code>www/</code>,
265
+ <code>app/</code>, or the project root) and reload — <code>dev</code> will serve it.
266
+ Point it at a different folder with <code>rastack dev --app &lt;dir&gt;</code>.</li>
267
+ <li>Wire your app to the local engine with
268
+ <code>&lt;RAStackProvider mode="local"&gt;</code>.</li>
269
+ <li>Live backend endpoints:
270
+ <a href="/schema.rastack.json">/schema.rastack.json</a>,
271
+ <a href="/openapi.json">/openapi.json</a>,
272
+ <code>/data/warehouse/…</code>, <code>/wasm/…</code>.</li>
273
+ <li>Want the database console instead? Run <code>rastack admin</code>.</li>
274
+ </ul>
275
+ <div class="foot">React API Stack — a full-stack framework on object storage.</div>
276
+ </div>
277
+ </body>
278
+ </html>
279
+ `;
280
+ }