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.
- package/CHANGELOG.md +32 -0
- package/README.md +19 -24
- package/admin/AdminApp.tsx +56 -3
- package/admin/httpEngine.ts +5 -6
- package/admin/index.tsx +11 -85
- package/admin/ui/icons.tsx +9 -0
- package/admin/ui/styles.ts +3 -0
- package/admin/useAdmin.ts +41 -4
- package/dist/admin.js +17 -22
- package/dist/compile/analyze.js +11 -1
- package/dist/compile/model.d.ts +1 -1
- package/dist/compile/run.d.ts +2 -2
- package/dist/compile/run.js +2 -2
- package/dist/define/index.d.ts +10 -4
- package/dist/define/index.js +2 -2
- package/dist/define/manifest.js +7 -1
- package/dist/deploy/io.d.ts +50 -0
- package/dist/deploy/io.js +124 -0
- package/dist/deploy/workflows.js +1 -1
- package/dist/dev/harness.d.ts +19 -27
- package/dist/dev/harness.js +31 -40
- package/dist/dev/native-server.d.ts +29 -0
- package/dist/dev/native-server.js +174 -0
- package/dist/external/envelope.d.ts +1 -1
- package/dist/external/envelope.js +1 -1
- package/dist/external/service.d.ts +2 -2
- package/dist/external/service.js +2 -2
- package/dist/external/types.d.ts +1 -1
- package/dist/external/types.js +1 -1
- package/dist/rastack-admin.d.ts +6 -14
- package/dist/rastack-admin.js +60 -88
- package/dist/rastack-bootstrap.js +18 -47
- package/dist/rastack-design.d.ts +1 -2
- package/dist/rastack-design.js +1 -2
- package/dist/rastack-dev.d.ts +7 -10
- package/dist/rastack-dev.js +68 -80
- package/dist/rastack-init.d.ts +6 -3
- package/dist/rastack-init.js +43 -30
- package/dist/rastack.d.ts +1 -1
- package/dist/rastack.js +5 -6
- package/dist/validate/transitions.d.ts +1 -1
- package/dist/validate/transitions.js +1 -1
- package/hooks/query/api.ts +4 -6
- package/package.json +2 -2
- package/provider/index.ts +0 -1
- package/provider/provider.tsx +31 -134
- package/provider/types.ts +30 -69
- package/src/compile/analyze.ts +11 -1
- package/src/compile/model.ts +1 -1
- package/src/compile/run.ts +2 -2
- package/src/define/index.ts +7 -4
- package/src/define/manifest.ts +7 -1
- package/src/deploy/io.ts +162 -1
- package/src/deploy/workflows.ts +1 -1
- package/src/dev/harness.ts +30 -43
- package/src/dev/native-server.ts +164 -0
- package/src/external/envelope.ts +1 -1
- package/src/external/service.ts +2 -2
- package/src/external/types.ts +1 -1
- package/src/rastack-admin.ts +66 -107
- package/src/rastack-bootstrap.ts +24 -69
- package/src/rastack-design.ts +1 -2
- package/src/rastack-dev.ts +79 -91
- package/src/rastack-init.ts +50 -41
- package/src/rastack.ts +5 -6
- package/src/validate/transitions.ts +1 -1
- package/dist/rastack-wasm-build.d.ts +0 -19
- package/dist/rastack-wasm-build.js +0 -94
- package/dist/wasm/package.json +0 -4
- package/dist/wasm/rastack_wasm.d.ts +0 -100
- package/dist/wasm/rastack_wasm.js +0 -745
- package/dist/wasm/rastack_wasm_bg.wasm +0 -0
- package/dist/wasm/rastack_wasm_bg.wasm.d.ts +0 -28
- package/provider/wasm.ts +0 -126
- package/scripts/copy-wasm.js +0 -35
- package/src/rastack-wasm-build.ts +0 -113
package/src/deploy/io.ts
CHANGED
|
@@ -6,11 +6,20 @@
|
|
|
6
6
|
|
|
7
7
|
import { execFileSync, execSync } from "child_process";
|
|
8
8
|
import * as fs from "fs";
|
|
9
|
+
import * as https from "https";
|
|
9
10
|
import * as os from "os";
|
|
10
11
|
import * as path from "path";
|
|
11
12
|
import * as readline from "readline";
|
|
12
13
|
import { CONFIG_PATH } from "./workflows";
|
|
13
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
BOOTSTRAP_ARTIFACTS_OUTPUT,
|
|
16
|
+
BOOTSTRAP_TEMPLATES_OUTPUT,
|
|
17
|
+
DeployConfig,
|
|
18
|
+
PartialDeployConfig,
|
|
19
|
+
bootstrapStackArgs,
|
|
20
|
+
bootstrapStackName,
|
|
21
|
+
normalizeConfig,
|
|
22
|
+
} from "./config";
|
|
14
23
|
import { zipStore } from "./zip";
|
|
15
24
|
|
|
16
25
|
/** Absolute path to the deploy config for a project root. */
|
|
@@ -50,6 +59,65 @@ export function resolveDeployAsset(
|
|
|
50
59
|
return null;
|
|
51
60
|
}
|
|
52
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Base URL the framework's CloudFormation templates are published at. The same
|
|
64
|
+
* site that hosts the installer (`reactapistack.com/setup.sh`) serves the base
|
|
65
|
+
* templates, so a CLI whose bundled copy is missing (a partial install, or a
|
|
66
|
+
* globally-linked dev build) can still fetch them.
|
|
67
|
+
*/
|
|
68
|
+
export const TEMPLATE_BASE_URL = "https://reactapistack.com/cloudformation";
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Download a published CloudFormation template to a temp file, returning its
|
|
72
|
+
* path — or null on any failure (offline, 404, write error). Used as the
|
|
73
|
+
* fallback when the template is not bundled with the install.
|
|
74
|
+
*/
|
|
75
|
+
export function downloadTemplate(
|
|
76
|
+
name: string,
|
|
77
|
+
base = TEMPLATE_BASE_URL,
|
|
78
|
+
): Promise<string | null> {
|
|
79
|
+
const url = `${base}/${name}`;
|
|
80
|
+
return new Promise((resolve) => {
|
|
81
|
+
https
|
|
82
|
+
.get(url, (res) => {
|
|
83
|
+
if (res.statusCode !== 200) {
|
|
84
|
+
res.resume();
|
|
85
|
+
resolve(null);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const chunks: Buffer[] = [];
|
|
89
|
+
res.on("data", (c: Buffer) => chunks.push(c));
|
|
90
|
+
res.on("end", () => {
|
|
91
|
+
try {
|
|
92
|
+
const dest = path.join(
|
|
93
|
+
os.tmpdir(),
|
|
94
|
+
`rastack-${path.basename(name)}`,
|
|
95
|
+
);
|
|
96
|
+
fs.writeFileSync(dest, Buffer.concat(chunks));
|
|
97
|
+
resolve(dest);
|
|
98
|
+
} catch {
|
|
99
|
+
resolve(null);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
})
|
|
103
|
+
.on("error", () => resolve(null));
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Resolve a CloudFormation template to a local file path for `aws cloudformation
|
|
109
|
+
* deploy` (which accepts only `--template-file`). Prefers the copy bundled with
|
|
110
|
+
* the `rastack` install; when that is missing, downloads it from the React API
|
|
111
|
+
* Stack website. Returns null only when the template is neither bundled nor
|
|
112
|
+
* fetchable.
|
|
113
|
+
*/
|
|
114
|
+
export async function resolveTemplateFile(
|
|
115
|
+
name: string,
|
|
116
|
+
cwd = process.cwd(),
|
|
117
|
+
): Promise<string | null> {
|
|
118
|
+
return resolveDeployAsset(name, cwd) ?? (await downloadTemplate(name));
|
|
119
|
+
}
|
|
120
|
+
|
|
53
121
|
/** Read + normalize the deploy config, or null when it does not exist yet. */
|
|
54
122
|
export function loadConfig(cwd = process.cwd()): DeployConfig | null {
|
|
55
123
|
const file = configPath(cwd);
|
|
@@ -121,6 +189,26 @@ export function awsAvailable(): boolean {
|
|
|
121
189
|
return awsCapture(["--version"]) !== null;
|
|
122
190
|
}
|
|
123
191
|
|
|
192
|
+
/**
|
|
193
|
+
* The AWS account id the CLI's active credentials resolve to, or null when the
|
|
194
|
+
* CLI has no usable credentials (not logged in, an expired SSO session, or no
|
|
195
|
+
* configured profile). `init`/`bootstrap` call this to fail fast with an
|
|
196
|
+
* actionable "log in" message before attempting a stack deploy, instead of
|
|
197
|
+
* letting the first `cloudformation deploy` fail with a cryptic AWS error.
|
|
198
|
+
*/
|
|
199
|
+
export function awsIdentity(region?: string): string | null {
|
|
200
|
+
const args = [
|
|
201
|
+
"sts",
|
|
202
|
+
"get-caller-identity",
|
|
203
|
+
"--query",
|
|
204
|
+
"Account",
|
|
205
|
+
"--output",
|
|
206
|
+
"text",
|
|
207
|
+
];
|
|
208
|
+
if (region) args.push("--region", region);
|
|
209
|
+
return awsCapture(args);
|
|
210
|
+
}
|
|
211
|
+
|
|
124
212
|
/**
|
|
125
213
|
* A single Output value of a CloudFormation stack, or null when the stack does
|
|
126
214
|
* not exist or the key is unset. `describe-stacks` prints `None` for a missing
|
|
@@ -176,6 +264,79 @@ export function uploadPlaceholder(
|
|
|
176
264
|
return true;
|
|
177
265
|
}
|
|
178
266
|
|
|
267
|
+
/**
|
|
268
|
+
* Deploy the tier-0 bootstrap stack, retrying once against an account that
|
|
269
|
+
* already has a GitHub OIDC provider — AWS allows only one provider per URL, so
|
|
270
|
+
* a second create is rejected and the retry tells the template to reuse the
|
|
271
|
+
* existing one. Throws on failure (after the retry). Shared by `rastack init`
|
|
272
|
+
* and `rastack bootstrap` so both stand the stack up identically.
|
|
273
|
+
*/
|
|
274
|
+
export function deployBootstrap(
|
|
275
|
+
cfg: DeployConfig,
|
|
276
|
+
reuseOidc: boolean,
|
|
277
|
+
template: string,
|
|
278
|
+
): void {
|
|
279
|
+
try {
|
|
280
|
+
awsRun(bootstrapStackArgs(cfg, reuseOidc, template));
|
|
281
|
+
} catch (err) {
|
|
282
|
+
if (reuseOidc) throw err;
|
|
283
|
+
console.log(
|
|
284
|
+
" • retrying bootstrap, reusing the account's existing GitHub OIDC provider",
|
|
285
|
+
);
|
|
286
|
+
awsRun(bootstrapStackArgs(cfg, true, template));
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Ensure the tier-0 bootstrap stack exists for this account+region, **creating
|
|
292
|
+
* it when missing**, and return its artifacts/templates buckets. This is why
|
|
293
|
+
* `rastack init` is a single command that stands up *everything* — the
|
|
294
|
+
* account-level bootstrap stack and the per-app backend — with no separate
|
|
295
|
+
* `rastack bootstrap` step. Idempotent: an existing stack is reconciled in
|
|
296
|
+
* place. Throws with an actionable message when the template cannot be resolved,
|
|
297
|
+
* the deploy fails, or the buckets can't be read back.
|
|
298
|
+
*/
|
|
299
|
+
export async function ensureBootstrapStack(
|
|
300
|
+
cfg: DeployConfig,
|
|
301
|
+
reuseOidc = false,
|
|
302
|
+
): Promise<{ artifactsBucket: string; templatesBucket: string | null }> {
|
|
303
|
+
const stack = bootstrapStackName(cfg);
|
|
304
|
+
const existing = stackOutput(stack, BOOTSTRAP_ARTIFACTS_OUTPUT, cfg.region);
|
|
305
|
+
if (existing) {
|
|
306
|
+
console.log(` ✓ bootstrap stack ${stack} already exists — reconciling`);
|
|
307
|
+
} else {
|
|
308
|
+
console.log(
|
|
309
|
+
`\n▸ Creating the tier-0 bootstrap stack ${stack} (@ ${cfg.region})`,
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const template = await resolveTemplateFile("bootstrap.yaml");
|
|
314
|
+
if (!template) {
|
|
315
|
+
throw new Error(
|
|
316
|
+
"bootstrap.yaml template not found in the rastack package and could not be\n" +
|
|
317
|
+
" downloaded from reactapistack.com. Reinstall rastack (`npm i -g rastack`).",
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
deployBootstrap(cfg, reuseOidc, template);
|
|
321
|
+
|
|
322
|
+
const artifactsBucket = stackOutput(
|
|
323
|
+
stack,
|
|
324
|
+
BOOTSTRAP_ARTIFACTS_OUTPUT,
|
|
325
|
+
cfg.region,
|
|
326
|
+
);
|
|
327
|
+
const templatesBucket = stackOutput(
|
|
328
|
+
stack,
|
|
329
|
+
BOOTSTRAP_TEMPLATES_OUTPUT,
|
|
330
|
+
cfg.region,
|
|
331
|
+
);
|
|
332
|
+
if (!artifactsBucket) {
|
|
333
|
+
throw new Error(
|
|
334
|
+
`bootstrap stack ${stack} deployed but its ArtifactsBucket output could not be read.`,
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
return { artifactsBucket, templatesBucket };
|
|
338
|
+
}
|
|
339
|
+
|
|
179
340
|
// ── GitHub CLI boundary ──────────────────────────────────────────────────────
|
|
180
341
|
// After the stack is up `init` wires the two repo Variables CI reads
|
|
181
342
|
// (AWS_DEPLOY_ROLE_ARN / AWS_REGION) through the developer's own `gh` — already
|
package/src/deploy/workflows.ts
CHANGED
|
@@ -124,7 +124,7 @@ jobs:
|
|
|
124
124
|
- name: Cache cargo
|
|
125
125
|
uses: Swatinem/rust-cache@v2
|
|
126
126
|
with:
|
|
127
|
-
workspaces:
|
|
127
|
+
workspaces: engine
|
|
128
128
|
# cargo-lambda cross-builds the custom-runtime binary; it auto-installs
|
|
129
129
|
# zig (via pip) for the cross-compile when needed.
|
|
130
130
|
- name: Install cargo-lambda
|
package/src/dev/harness.ts
CHANGED
|
@@ -1,20 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Pure core of `rastack dev` — the
|
|
2
|
+
* Pure core of `rastack dev` — local development against the native Rust server.
|
|
3
3
|
*
|
|
4
|
-
* `rastack dev`
|
|
5
|
-
* (the same
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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`.
|
|
4
|
+
* `rastack dev` serves **your app** from Node and proxies `/api` to a native
|
|
5
|
+
* `rastack-server` process (the same `rastack-api-core` Lambda runs). The
|
|
6
|
+
* browser renders the UI. It does not execute the engine. `rastack admin` is
|
|
7
|
+
* the separate command that starts `rastack-server admin`.
|
|
10
8
|
*
|
|
11
9
|
* When the project has no app to serve, `dev` shows a Django-style welcome
|
|
12
10
|
* page (`renderWelcome`) so a fresh scaffold still boots to something friendly.
|
|
13
11
|
*
|
|
14
|
-
* Everything decidable without IO lives here (arg parsing, MIME types,
|
|
15
|
-
* the
|
|
16
|
-
* without a socket or a filesystem; `rastack-dev.ts` is the
|
|
17
|
-
*
|
|
12
|
+
* Everything decidable without IO lives here (arg parsing, MIME types, where
|
|
13
|
+
* the native binary is looked up, resolving the app root, the HTML pages) so
|
|
14
|
+
* it is unit-tested without a socket or a filesystem; `rastack-dev.ts` is the
|
|
15
|
+
* thin IO wrapper that compiles, starts the server, and serves.
|
|
18
16
|
*/
|
|
19
17
|
|
|
20
18
|
import * as path from "path";
|
|
@@ -24,7 +22,7 @@ export interface DevArgs {
|
|
|
24
22
|
resourcesDir: string;
|
|
25
23
|
/** Where `rastack compile` writes schema.rastack.json + openapi.json. */
|
|
26
24
|
outDir: string;
|
|
27
|
-
/** The
|
|
25
|
+
/** The on-disk Iceberg warehouse the native server opens. */
|
|
28
26
|
warehouseDir: string;
|
|
29
27
|
/**
|
|
30
28
|
* Explicit app root to serve at `/` (its `index.html`). Empty when unset —
|
|
@@ -139,7 +137,6 @@ const MIME: Record<string, string> = {
|
|
|
139
137
|
".mjs": "text/javascript; charset=utf-8",
|
|
140
138
|
".css": "text/css; charset=utf-8",
|
|
141
139
|
".json": "application/json; charset=utf-8",
|
|
142
|
-
// Critical: `WebAssembly.instantiateStreaming` requires this exact type.
|
|
143
140
|
".wasm": "application/wasm",
|
|
144
141
|
".text": "text/plain; charset=utf-8",
|
|
145
142
|
".parquet": "application/octet-stream",
|
|
@@ -152,32 +149,22 @@ export function contentType(filePath: string): string {
|
|
|
152
149
|
}
|
|
153
150
|
|
|
154
151
|
/**
|
|
155
|
-
* Ordered
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
* cargo; the repo-local `wasm/` (from a source build) and a sibling of the
|
|
159
|
-
* running module are fallbacks for development. `dirnameDir` is the directory of
|
|
160
|
-
* the compiled `rastack-dev.js` (`dist/`), so `../wasm` and `./wasm` cover both
|
|
161
|
-
* `dist/rastack-dev.js` → `dist/wasm` and `dist/dev/…` layouts.
|
|
152
|
+
* Ordered paths for the native `rastack-server` binary inside a workspace
|
|
153
|
+
* checkout. Release wins over debug. `RASTACK_SERVER` and `PATH` are checked
|
|
154
|
+
* by the IO wrapper, not here.
|
|
162
155
|
*/
|
|
163
|
-
export function
|
|
164
|
-
dirnameDir: string,
|
|
165
|
-
cwd = process.cwd(),
|
|
166
|
-
): string[] {
|
|
156
|
+
export function serverBinaryCandidates(cwd: string): string[] {
|
|
167
157
|
return [
|
|
168
|
-
path.join(
|
|
169
|
-
path.join(
|
|
170
|
-
path.join(cwd, "wasm"),
|
|
158
|
+
path.join(cwd, "engine", "target", "release", "rastack-server"),
|
|
159
|
+
path.join(cwd, "engine", "target", "debug", "rastack-server"),
|
|
171
160
|
];
|
|
172
161
|
}
|
|
173
162
|
|
|
174
|
-
/** The name of the file that marks a directory as a usable wasm bundle. */
|
|
175
|
-
export const WASM_ENTRY = "rastack_wasm.js";
|
|
176
|
-
|
|
177
163
|
/**
|
|
178
164
|
* The HTML shell served at `/` by **`rastack admin`**. It loads the bundled
|
|
179
|
-
* React admin (`/dev/admin.js`)
|
|
180
|
-
*
|
|
165
|
+
* React admin (`/dev/admin.js`). `rastack admin` itself does not serve this
|
|
166
|
+
* page: the native `rastack-server admin` process serves `/admin/` and the
|
|
167
|
+
* bundle talks to that process over HTTP.
|
|
181
168
|
* Deliberately tiny — all logic lives in the bundle — mirroring the
|
|
182
169
|
* `rastack design` studio. `rastack dev` does **not** use this: it serves the
|
|
183
170
|
* developer's own app (or the welcome page below).
|
|
@@ -223,9 +210,8 @@ export function renderShell(title = "React API Stack — admin"): string {
|
|
|
223
210
|
/**
|
|
224
211
|
* The Django-style welcome page `rastack dev` serves at `/` when the project
|
|
225
212
|
* has **no app** to serve (no `index.html` in any `appRootCandidates` dir).
|
|
226
|
-
* It confirms the dev server is up, points at the
|
|
227
|
-
*
|
|
228
|
-
* "It worked!" moment of `django-admin runserver`, on brand for the stack.
|
|
213
|
+
* It confirms the dev server is up, points at the native API proxied at
|
|
214
|
+
* `/api`, and tells you how to add an app or open the admin.
|
|
229
215
|
*/
|
|
230
216
|
export function renderWelcome(title = "React API Stack — dev"): string {
|
|
231
217
|
return `<!doctype html>
|
|
@@ -272,20 +258,21 @@ export function renderWelcome(title = "React API Stack — dev"): string {
|
|
|
272
258
|
<div class="rocket">🚀</div>
|
|
273
259
|
<h1>The dev server is running.</h1>
|
|
274
260
|
<p class="lead">
|
|
275
|
-
Congratulations — <strong>rastack dev</strong> is up
|
|
276
|
-
|
|
277
|
-
app to serve yet.
|
|
261
|
+
Congratulations — <strong>rastack dev</strong> is up. The API is a native
|
|
262
|
+
Rust process (<code>rastack-server</code>) on this machine. This page is
|
|
263
|
+
only the UI. You're seeing it because there's no app to serve yet.
|
|
278
264
|
</p>
|
|
279
265
|
<ul>
|
|
280
266
|
<li>Add an <code>index.html</code> (in <code>public/</code>, <code>www/</code>,
|
|
281
267
|
<code>app/</code>, or the project root) and reload — <code>dev</code> will serve it.
|
|
282
268
|
Point it at a different folder with <code>rastack dev --app <dir></code>.</li>
|
|
283
|
-
<li>Wire your app
|
|
284
|
-
<code><RAStackProvider mode="local"></code
|
|
285
|
-
|
|
269
|
+
<li>Wire your app with
|
|
270
|
+
<code><RAStackProvider mode="local"></code>. Requests go to
|
|
271
|
+
<code>/api</code> on this origin, which is proxied to the native server.</li>
|
|
272
|
+
<li>Live files:
|
|
286
273
|
<a href="/schema.rastack.json">/schema.rastack.json</a>,
|
|
287
|
-
<a href="/openapi.json">/openapi.json</a
|
|
288
|
-
|
|
274
|
+
<a href="/openapi.json">/openapi.json</a>.
|
|
275
|
+
The warehouse stays on disk; the browser does not load it.</li>
|
|
289
276
|
<li>Want the database console instead? Run <code>rastack admin</code>.</li>
|
|
290
277
|
</ul>
|
|
291
278
|
<div class="foot">React API Stack — a full-stack framework on object storage.</div>
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Start the native `rastack-server` binary — the same `rastack-api-core`
|
|
3
|
+
* process Lambda runs. The browser never executes this binary. `rastack dev`
|
|
4
|
+
* proxies `/api` to it; `rastack admin` lets the binary serve `/admin` itself.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { spawn, spawnSync, type ChildProcess } from "child_process";
|
|
8
|
+
import * as fs from "fs";
|
|
9
|
+
import * as net from "net";
|
|
10
|
+
import * as path from "path";
|
|
11
|
+
import { serverBinaryCandidates } from "./harness";
|
|
12
|
+
|
|
13
|
+
export interface NativeServerOpts {
|
|
14
|
+
/** Absolute path to the `rastack-server` binary. */
|
|
15
|
+
bin: string;
|
|
16
|
+
/** `serve` is the API. `admin` mounts the React console at `/admin`. */
|
|
17
|
+
command: "serve" | "admin";
|
|
18
|
+
manifest: string;
|
|
19
|
+
warehouse: string;
|
|
20
|
+
/** `host:port` the binary binds. */
|
|
21
|
+
addr: string;
|
|
22
|
+
/** Directory that contains `admin.js` (`RASTACK_ADMIN_ASSETS`). */
|
|
23
|
+
adminAssets?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const MISSING = `
|
|
27
|
+
✗ No native rastack-server binary.
|
|
28
|
+
The API does not run in the browser. rastack dev and rastack admin start
|
|
29
|
+
the native Rust server (rastack-api-core) and the page talks to it over HTTP.
|
|
30
|
+
Build it from this repo:
|
|
31
|
+
|
|
32
|
+
cargo build -p rastack-server --manifest-path engine/Cargo.toml
|
|
33
|
+
|
|
34
|
+
Or set RASTACK_SERVER to the binary. There is no in-browser engine.
|
|
35
|
+
`;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* `rastack-server` on PATH. Walks the directories ourselves so the lookup
|
|
39
|
+
* never goes through a shell (`command -v` with `shell: true`).
|
|
40
|
+
*/
|
|
41
|
+
function serverOnPath(): string | null {
|
|
42
|
+
const entries = (process.env.PATH ?? "").split(path.delimiter);
|
|
43
|
+
for (const dir of entries) {
|
|
44
|
+
if (!dir) continue;
|
|
45
|
+
const candidate = path.join(dir, "rastack-server");
|
|
46
|
+
try {
|
|
47
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
48
|
+
} catch {
|
|
49
|
+
// An unreadable PATH entry is not a candidate.
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** `RASTACK_SERVER`, a workspace target dir, or `rastack-server` on PATH. */
|
|
56
|
+
export function resolveServerBinary(cwd: string): string | null {
|
|
57
|
+
const fromEnv = process.env.RASTACK_SERVER;
|
|
58
|
+
if (fromEnv && fs.existsSync(fromEnv)) return fromEnv;
|
|
59
|
+
for (const candidate of serverBinaryCandidates(cwd)) {
|
|
60
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
61
|
+
}
|
|
62
|
+
return serverOnPath();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function cargoAvailable(): boolean {
|
|
66
|
+
try {
|
|
67
|
+
return spawnSync("cargo", ["--version"], { stdio: "ignore" }).status === 0;
|
|
68
|
+
} catch {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Locate the native binary, building it with cargo when this repo's workspace
|
|
75
|
+
* is present and the binary is not. Throws when nothing native can be started.
|
|
76
|
+
*/
|
|
77
|
+
export function ensureServerBinary(cwd: string): string {
|
|
78
|
+
const ready = resolveServerBinary(cwd);
|
|
79
|
+
if (ready) return ready;
|
|
80
|
+
const manifest = path.join(cwd, "engine", "Cargo.toml");
|
|
81
|
+
if (fs.existsSync(manifest) && cargoAvailable()) {
|
|
82
|
+
console.log(
|
|
83
|
+
"\n▸ Building native rastack-server (cargo build -p rastack-server)…\n",
|
|
84
|
+
);
|
|
85
|
+
const built = spawnSync(
|
|
86
|
+
"cargo",
|
|
87
|
+
["build", "-p", "rastack-server", "--manifest-path", manifest],
|
|
88
|
+
{ stdio: "inherit" },
|
|
89
|
+
);
|
|
90
|
+
if (built.status === 0) {
|
|
91
|
+
const after = resolveServerBinary(cwd);
|
|
92
|
+
if (after) return after;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
throw new Error(MISSING.trim());
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Spawn `rastack-server serve|admin`. Stdio is inherited. */
|
|
99
|
+
export function startNativeServer(opts: NativeServerOpts): ChildProcess {
|
|
100
|
+
const env: NodeJS.ProcessEnv = { ...process.env };
|
|
101
|
+
if (opts.adminAssets) env.RASTACK_ADMIN_ASSETS = opts.adminAssets;
|
|
102
|
+
// `bin` is resolveServerBinary's result: RASTACK_SERVER (a file that exists),
|
|
103
|
+
// a workspace target path, or a PATH entry named rastack-server. It is not
|
|
104
|
+
// request input. The command cannot be a string literal because the path
|
|
105
|
+
// depends on which of those three resolved. The nosemgrep comment has to sit
|
|
106
|
+
// on the line directly above the tainted argument — that is the line Semgrep
|
|
107
|
+
// reports, not the `spawn(` line.
|
|
108
|
+
const bin = opts.bin;
|
|
109
|
+
return spawn(
|
|
110
|
+
// nosemgrep: javascript.lang.security.detect-child-process.detect-child-process
|
|
111
|
+
bin,
|
|
112
|
+
[
|
|
113
|
+
opts.command,
|
|
114
|
+
"--manifest",
|
|
115
|
+
opts.manifest,
|
|
116
|
+
"--warehouse",
|
|
117
|
+
opts.warehouse,
|
|
118
|
+
"--addr",
|
|
119
|
+
opts.addr,
|
|
120
|
+
"--insecure-dev-auth",
|
|
121
|
+
],
|
|
122
|
+
{ stdio: "inherit", env },
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Resolve once the process accepts TCP, or reject if it exits or times out. */
|
|
127
|
+
export function waitForTcp(
|
|
128
|
+
port: number,
|
|
129
|
+
child: ChildProcess,
|
|
130
|
+
timeoutMs = 30_000,
|
|
131
|
+
): Promise<void> {
|
|
132
|
+
return new Promise((resolve, reject) => {
|
|
133
|
+
const deadline = Date.now() + timeoutMs;
|
|
134
|
+
let settled = false;
|
|
135
|
+
const finish = (err?: Error) => {
|
|
136
|
+
if (settled) return;
|
|
137
|
+
settled = true;
|
|
138
|
+
if (err) reject(err);
|
|
139
|
+
else resolve();
|
|
140
|
+
};
|
|
141
|
+
child.once("exit", (code) =>
|
|
142
|
+
finish(
|
|
143
|
+
new Error(
|
|
144
|
+
`rastack-server exited (${code ?? "?"}) before it accepted connections`,
|
|
145
|
+
),
|
|
146
|
+
),
|
|
147
|
+
);
|
|
148
|
+
const tick = () => {
|
|
149
|
+
if (settled) return;
|
|
150
|
+
if (Date.now() > deadline) {
|
|
151
|
+
finish(
|
|
152
|
+
new Error(`rastack-server did not listen on 127.0.0.1:${port}`),
|
|
153
|
+
);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const socket = net.connect(port, "127.0.0.1", () => {
|
|
157
|
+
socket.end();
|
|
158
|
+
finish();
|
|
159
|
+
});
|
|
160
|
+
socket.on("error", () => setTimeout(tick, 100));
|
|
161
|
+
};
|
|
162
|
+
tick();
|
|
163
|
+
});
|
|
164
|
+
}
|
package/src/external/envelope.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Normalise an upstream response into rastack's REST contract — the DRF-shaped
|
|
3
3
|
* list envelope and bare detail object the typed hooks and `<DataTable>` expect,
|
|
4
|
-
* identical to what the Rust API
|
|
4
|
+
* identical to what the native Rust API emits.
|
|
5
5
|
*
|
|
6
6
|
* External APIs rarely page the way rastack does (GitHub uses `per_page` + Link
|
|
7
7
|
* headers; a GraphQL list just returns an array), so the envelope is synthesised
|
package/src/external/service.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The mini local proxy service — the "lambda you run while iterating".
|
|
3
3
|
*
|
|
4
|
-
* `rastack dev`
|
|
5
|
-
*
|
|
4
|
+
* `rastack dev` proxies the stack's own resources to the native
|
|
5
|
+
* `rastack-server`. This adds the other half for **external** resources: a request
|
|
6
6
|
* handler it mounts under `/api/`, so `useData("github.repo")` works locally,
|
|
7
7
|
* cached, before any real Lambda is deployed. It is a thin adapter over the pure
|
|
8
8
|
* {@link handleExternalRequest} core — parse the path, resolve dev auth from env
|
package/src/external/types.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* External-API bindings — how a rastack `app.model` is served from an upstream
|
|
3
3
|
* REST or GraphQL API.
|
|
4
4
|
*
|
|
5
|
-
* The stack's own resources are served by the Rust API
|
|
5
|
+
* The stack's own resources are served by the native Rust API over the
|
|
6
6
|
* `/api/{app}/v1/{model}/` contract. An **external** resource (a GitHub repo, an
|
|
7
7
|
* Expo project, an AWS stack) is served by the proxy instead: it speaks the same
|
|
8
8
|
* contract to the client, and translates each request into an upstream call,
|