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