rastack 0.0.55 → 0.0.56

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,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
+ }
@@ -1,18 +1,22 @@
1
1
  #!/usr/bin/env node
2
2
 
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
  */
@@ -22,10 +26,12 @@ import * as fs from "fs";
22
26
  import * as http from "http";
23
27
  import * as path from "path";
24
28
  import {
29
+ APP_ENTRY,
25
30
  WASM_ENTRY,
31
+ appRootCandidates,
26
32
  contentType,
27
33
  parseDevArgs,
28
- renderShell,
34
+ renderWelcome,
29
35
  wasmBundleCandidates,
30
36
  } from "./dev/harness";
31
37
 
@@ -54,6 +60,14 @@ function cargoAvailable(): boolean {
54
60
  }
55
61
  }
56
62
 
63
+ /** First app-root candidate that holds an `index.html`, or null (→ welcome). */
64
+ function resolveAppRoot(appDir: string): string | null {
65
+ for (const dir of appRootCandidates(appDir)) {
66
+ if (fs.existsSync(path.join(dir, APP_ENTRY))) return dir;
67
+ }
68
+ return null;
69
+ }
70
+
57
71
  /** Locate the prebuilt wasm bundle dir, building from source as a fallback. */
58
72
  function resolveWasmDir(): string | null {
59
73
  for (const dir of wasmBundleCandidates(HERE)) {
@@ -130,7 +144,7 @@ function openBrowser(url: string): void {
130
144
  function main(argv: string[]): void {
131
145
  const args = parseDevArgs(argv);
132
146
 
133
- console.log(`\n rastack dev — the app + admin, in your browser over WASM\n`);
147
+ console.log(`\n rastack dev — your app, in your browser over WASM\n`);
134
148
  console.log(`▸ Compiling ${args.resourcesDir} → ${args.outDir}`);
135
149
  if (!compile(args.resourcesDir, args.outDir)) {
136
150
  console.error(" ✗ compile failed — fix the resource graph and re-run.");
@@ -149,13 +163,8 @@ function main(argv: string[]): void {
149
163
  process.exit(1);
150
164
  }
151
165
 
152
- const adminBundle = path.join(HERE, "admin.js");
153
- if (!fs.existsSync(adminBundle)) {
154
- console.error(
155
- " ✗ admin bundle (dist/admin.js) not found — reinstall rastack.",
156
- );
157
- process.exit(1);
158
- }
166
+ // Serve the developer's own app when there is one; otherwise the welcome page.
167
+ const appRoot = resolveAppRoot(args.appDir);
159
168
 
160
169
  const server = http.createServer((req, res) => {
161
170
  const url = (req.url || "/").split("?")[0];
@@ -163,13 +172,7 @@ function main(argv: string[]): void {
163
172
  res.writeHead(405).end("Method not allowed");
164
173
  return;
165
174
  }
166
- if (url === "/" || url === "/index.html") {
167
- res
168
- .writeHead(200, { "Content-Type": "text/html; charset=utf-8" })
169
- .end(renderShell());
170
- return;
171
- }
172
- if (url === "/dev/admin.js") return serveFile(res, adminBundle);
175
+ // Reserved backend routes always win over the app's static files.
173
176
  if (url.startsWith("/wasm/"))
174
177
  return serveUnder(res, wasmDir, url.slice("/wasm/".length));
175
178
  if (url === "/schema.rastack.json" || url === "/openapi.json")
@@ -180,14 +183,43 @@ function main(argv: string[]): void {
180
183
  args.warehouseDir,
181
184
  url.slice("/data/warehouse/".length),
182
185
  );
183
- res.writeHead(404).end("Not found");
186
+
187
+ // No app → the Django-style welcome page (root only; else 404).
188
+ if (!appRoot) {
189
+ if (url === "/" || url === "/index.html") {
190
+ res
191
+ .writeHead(200, { "Content-Type": "text/html; charset=utf-8" })
192
+ .end(renderWelcome());
193
+ return;
194
+ }
195
+ res.writeHead(404).end("Not found");
196
+ return;
197
+ }
198
+
199
+ // Serve the app: its index.html at `/`, an existing file as-is, and any
200
+ // other route back to index.html (SPA client-side routing).
201
+ if (url === "/" || url === "/index.html")
202
+ return serveFile(res, path.join(appRoot, APP_ENTRY));
203
+ const rel = decodeURIComponent(url).replace(/^\/+/, "");
204
+ const target = path.resolve(appRoot, rel);
205
+ const safeRoot = path.resolve(appRoot);
206
+ const inRoot =
207
+ target === safeRoot || target.startsWith(safeRoot + path.sep);
208
+ if (inRoot && fs.existsSync(target) && fs.statSync(target).isFile())
209
+ return serveFile(res, target);
210
+ return serveFile(res, path.join(appRoot, APP_ENTRY));
184
211
  });
185
212
 
186
213
  server.listen(args.port, () => {
187
214
  const url = `http://localhost:${args.port}`;
188
215
  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");
216
+ if (appRoot)
217
+ console.log(` ✓ app: ${path.relative(process.cwd(), appRoot) || "."}`);
218
+ else console.log(` ○ no app found — serving the welcome page`);
219
+ console.log(`\n ▸ ${url}\n`);
220
+ console.log(
221
+ " Need the database console? Run `rastack admin`. Ctrl+C to stop.\n",
222
+ );
191
223
  if (args.open) openBrowser(url);
192
224
  });
193
225
  }
@@ -64,10 +64,17 @@ function runNpm(
64
64
  args: string[],
65
65
  opts: { stdio: "inherit" | ["ignore", "pipe", "pipe"]; env?: NodeJS.ProcessEnv },
66
66
  ): Buffer | null {
67
- // nosemgrep: javascript.lang.security.detect-child-process.detect-child-process
68
- return NPM_SHELL
69
- ? execSync(npmShellCommand(NPM, args), opts)
70
- : execFileSync(NPM, args, opts);
67
+ if (NPM_SHELL) {
68
+ // Windows: launch the `npm.cmd` shim through a shell (a bare `.cmd` spawn is
69
+ // rejected with EINVAL since the Node CVE-2024-27980 fix). The only
70
+ // non-literal token — the version — is sanitized in `parseUpdateFlags`, and
71
+ // every other arg is a fixed literal, so nothing injectable reaches it. The
72
+ // nosemgrep must sit on the line directly above the `execSync` call for the
73
+ // engine to honour it.
74
+ // nosemgrep: javascript.lang.security.detect-child-process.detect-child-process
75
+ return execSync(npmShellCommand(NPM, args), opts);
76
+ }
77
+ return execFileSync(NPM, args, opts);
71
78
  }
72
79
 
73
80
  /** Capture `npm <args>` stdout, or null when npm is absent / the call fails. */
package/src/rastack.ts CHANGED
@@ -11,7 +11,7 @@
11
11
  * rastack generate [--schema-only] Generate typed hooks from OpenAPI
12
12
  * rastack tokens [input] [outDir] Compile design tokens → CSS vars + typed theme
13
13
  * 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
14
+ * rastack dev [--port n] [--warehouse d] Local dev: run your app in-browser over WASM
15
15
  * rastack admin [--warehouse dir] [--port n] Full admin console — React UI (rastack/components) over the native server
16
16
  * rastack import <file> --resource app.model Load CSV/XLSX/PDF data and validate it (state machine)
17
17
  * rastack scan [files...] [--fail-on-pii] Scan CSVs for PII and schema info
@@ -72,8 +72,9 @@ switch (command) {
72
72
  run("rastack-compile.js", [command, ...rest]);
73
73
  break;
74
74
  case "dev":
75
- // Local development: serve the app + admin in the browser over the WASM
76
- // engine (no native server, no cargo). Replaces `serve` + `wasm`.
75
+ // Local development: serve your app in the browser over the WASM engine
76
+ // (no native server, no cargo). The admin console is a separate command,
77
+ // `rastack admin`. Replaces `serve` + `wasm`.
77
78
  run("rastack-dev.js", rest);
78
79
  break;
79
80
  case "admin":
@@ -139,7 +140,7 @@ switch (command) {
139
140
  ` rastack generate [--schema-only]\n` +
140
141
  ` rastack tokens [input] [outDir] [--prefix p] Compile design tokens → CSS vars + typed theme\n` +
141
142
  ` rastack design [input] [--port n] Live design-system studio (showcase + edit)\n` +
142
- ` rastack dev [--port n] [--warehouse dir] [--no-open] Run the app + admin in-browser over WASM (local dev)\n` +
143
+ ` rastack dev [--port n] [--warehouse dir] [--app dir] [--no-open] Run your app in-browser over WASM (local dev)\n` +
143
144
  ` rastack admin [--warehouse dir] [--port n] [--no-open] Full admin console (React UI over the native server; needs a built rastack-server)\n` +
144
145
  ` rastack import <file.csv|.xlsx|.pdf> --resource app.model [--related app.model=<file>] [--out cleaned.json]\n` +
145
146
  ` rastack scan [files...] [--fail-on-pii]\n` +
package/test/dev.spec.ts CHANGED
@@ -1,11 +1,14 @@
1
1
  import {
2
2
  ADMIN_DEFAULTS,
3
+ APP_ENTRY,
3
4
  DEV_DEFAULTS,
4
5
  WASM_ENTRY,
6
+ appRootCandidates,
5
7
  contentType,
6
8
  parseAdminArgs,
7
9
  parseDevArgs,
8
10
  renderShell,
11
+ renderWelcome,
9
12
  wasmBundleCandidates,
10
13
  } from "../src/dev/harness";
11
14
  import {
@@ -57,6 +60,7 @@ describe("rastack dev — arg parsing", () => {
57
60
  resourcesDir: "app/resources",
58
61
  outDir: "build",
59
62
  warehouseDir: "data/wh",
63
+ appDir: "",
60
64
  port: 5000,
61
65
  open: false,
62
66
  });
@@ -114,7 +118,7 @@ describe("rastack dev — wasm bundle resolution", () => {
114
118
  });
115
119
  });
116
120
 
117
- describe("rastack dev — HTML shell", () => {
121
+ describe("rastack admin — HTML shell", () => {
118
122
  it("mounts #root and loads the bundled admin", () => {
119
123
  const html = renderShell();
120
124
  expect(html).toContain('<div id="root">');
@@ -123,6 +127,43 @@ describe("rastack dev — HTML shell", () => {
123
127
  });
124
128
  });
125
129
 
130
+ describe("rastack dev — app resolution", () => {
131
+ it("defaults appDir to empty (probe the conventional roots)", () => {
132
+ expect(DEV_DEFAULTS.appDir).toBe("");
133
+ });
134
+
135
+ it("reads --app as an explicit app root", () => {
136
+ expect(parseDevArgs(["--app", "site"]).appDir).toBe("site");
137
+ });
138
+
139
+ it("probes public/www/app/root when no --app is given", () => {
140
+ const dirs = appRootCandidates("", "/repo");
141
+ expect(dirs).toEqual([
142
+ "/repo/public",
143
+ "/repo/www",
144
+ "/repo/app",
145
+ "/repo",
146
+ ]);
147
+ expect(APP_ENTRY).toBe("index.html");
148
+ });
149
+
150
+ it("uses only the explicit --app dir when set", () => {
151
+ expect(appRootCandidates("site", "/repo")).toEqual(["/repo/site"]);
152
+ });
153
+ });
154
+
155
+ describe("rastack dev — welcome page (no app)", () => {
156
+ it("is a self-contained page pointing at the backend and admin", () => {
157
+ const html = renderWelcome();
158
+ expect(html.startsWith("<!doctype html>")).toBe(true);
159
+ // Django-style "it worked" confirmation, not the admin bundle.
160
+ expect(html).not.toContain("/dev/admin.js");
161
+ expect(html).toContain("dev server is running");
162
+ expect(html).toContain("/schema.rastack.json");
163
+ expect(html).toContain("rastack admin");
164
+ });
165
+ });
166
+
126
167
  // -- admin adapters ---------------------------------------------------------
127
168
 
128
169
  /** The in-repo airline fixtures, in manifest JSON shape. */
Binary file