rastack 0.0.42 → 0.0.44

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.
Files changed (41) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/admin/AdminApp.tsx +308 -0
  3. package/admin/adapters.ts +247 -0
  4. package/admin/index.tsx +88 -0
  5. package/admin/useAdmin.ts +299 -0
  6. package/dist/admin.js +81 -0
  7. package/dist/dev/harness.d.ts +53 -0
  8. package/dist/dev/harness.js +151 -0
  9. package/dist/rastack-dev.d.ts +18 -0
  10. package/dist/rastack-dev.js +197 -0
  11. package/dist/rastack-init.d.ts +3 -2
  12. package/dist/rastack-init.js +23 -44
  13. package/dist/rastack-update.js +16 -8
  14. package/dist/rastack-wasm-build.d.ts +9 -4
  15. package/dist/rastack-wasm-build.js +44 -33
  16. package/dist/rastack.d.ts +1 -2
  17. package/dist/rastack.js +7 -10
  18. package/dist/update.d.ts +11 -0
  19. package/dist/update.js +14 -0
  20. package/dist/wasm/package.json +4 -0
  21. package/dist/wasm/rastack_wasm.d.ts +100 -0
  22. package/dist/wasm/rastack_wasm.js +745 -0
  23. package/dist/wasm/rastack_wasm_bg.wasm +0 -0
  24. package/dist/wasm/rastack_wasm_bg.wasm.d.ts +28 -0
  25. package/package.json +6 -2
  26. package/scripts/build-admin.js +48 -0
  27. package/scripts/copy-wasm.js +35 -0
  28. package/src/dev/harness.ts +130 -0
  29. package/src/rastack-dev.ts +197 -0
  30. package/src/rastack-init.ts +26 -62
  31. package/src/rastack-update.ts +22 -9
  32. package/src/rastack-wasm-build.ts +52 -41
  33. package/src/rastack.ts +7 -10
  34. package/src/update.ts +14 -0
  35. package/test/dev.spec.ts +277 -0
  36. package/test/update.spec.ts +13 -0
  37. package/wasm/package.json +4 -0
  38. package/wasm/rastack_wasm.d.ts +100 -0
  39. package/wasm/rastack_wasm.js +745 -0
  40. package/wasm/rastack_wasm_bg.wasm +0 -0
  41. package/wasm/rastack_wasm_bg.wasm.d.ts +28 -0
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Pure core of `rastack dev` — the local, always-WASM development server.
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
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.
9
+ *
10
+ * 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.
14
+ */
15
+ export interface DevArgs {
16
+ /** Where the TypeScript resource definitions live (compiled on start). */
17
+ resourcesDir: string;
18
+ /** Where `rastack compile` writes schema.rastack.json + openapi.json. */
19
+ outDir: string;
20
+ /** The committed Iceberg warehouse served to seed the browser engine. */
21
+ warehouseDir: string;
22
+ /** Port the dev server listens on. */
23
+ port: number;
24
+ /** Open the browser on start (false with `--no-open`). */
25
+ open: boolean;
26
+ }
27
+ export declare const DEV_DEFAULTS: DevArgs;
28
+ /**
29
+ * Parse `rastack dev` args: `--port`, `--warehouse`, `--out`, `--no-open`, and a
30
+ * single positional resources dir. Unknown flags are ignored (forgiving CLI).
31
+ */
32
+ export declare function parseDevArgs(argv: string[]): DevArgs;
33
+ /** MIME type for a served path, defaulting to octet-stream (Iceberg data files). */
34
+ export declare function contentType(filePath: string): string;
35
+ /**
36
+ * Ordered directories to look for the prebuilt wasm bundle (`rastack_wasm.js` +
37
+ * `rastack_wasm_bg.wasm`). The bundle is shipped inside the package
38
+ * (`dist/wasm/`, copied at build), so `rastack dev` works from any repo with no
39
+ * cargo; the repo-local `tools/wasm/` (from a source build) and a sibling of the
40
+ * running module are fallbacks for development. `dirnameDir` is the directory of
41
+ * the compiled `rastack-dev.js` (`dist/`), so `../wasm` and `./wasm` cover both
42
+ * `dist/rastack-dev.js` → `dist/wasm` and `dist/dev/…` layouts.
43
+ */
44
+ export declare function wasmBundleCandidates(dirnameDir: string, cwd?: string): string[];
45
+ /** The name of the file that marks a directory as a usable wasm bundle. */
46
+ export declare const WASM_ENTRY = "rastack_wasm.js";
47
+ /**
48
+ * The HTML shell served at `/`. It loads the bundled React admin
49
+ * (`/dev/admin.js`), which fetches the schema/openapi, boots the wasm engine,
50
+ * seeds it from `/data/warehouse/`, and renders the admin. Deliberately tiny —
51
+ * all logic lives in the bundle — mirroring the `rastack design` studio.
52
+ */
53
+ export declare function renderShell(title?: string): string;
@@ -0,0 +1,151 @@
1
+ "use strict";
2
+ /**
3
+ * Pure core of `rastack dev` — the local, always-WASM development server.
4
+ *
5
+ * `rastack dev` boots the app + admin **in the browser** over the WebAssembly
6
+ * engine (the same app-generic `rastack-api-core` the Lambda runs, fed the app's
7
+ * compiled manifest). No native server, no cargo: a dependency-free static
8
+ * server hands the browser the prebuilt wasm engine, the compiled schema, and
9
+ * the committed Iceberg warehouse, and a bundled React admin drives it.
10
+ *
11
+ * Everything decidable without IO lives here (arg parsing, MIME types, locating
12
+ * the wasm bundle, the HTML shell) so it is unit-tested without a socket or a
13
+ * filesystem; `rastack-dev.ts` is the thin IO wrapper that compiles, resolves
14
+ * the engine, and serves.
15
+ */
16
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
17
+ if (k2 === undefined) k2 = k;
18
+ var desc = Object.getOwnPropertyDescriptor(m, k);
19
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
20
+ desc = { enumerable: true, get: function() { return m[k]; } };
21
+ }
22
+ Object.defineProperty(o, k2, desc);
23
+ }) : (function(o, m, k, k2) {
24
+ if (k2 === undefined) k2 = k;
25
+ o[k2] = m[k];
26
+ }));
27
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
28
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
29
+ }) : function(o, v) {
30
+ o["default"] = v;
31
+ });
32
+ var __importStar = (this && this.__importStar) || (function () {
33
+ var ownKeys = function(o) {
34
+ ownKeys = Object.getOwnPropertyNames || function (o) {
35
+ var ar = [];
36
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
37
+ return ar;
38
+ };
39
+ return ownKeys(o);
40
+ };
41
+ return function (mod) {
42
+ if (mod && mod.__esModule) return mod;
43
+ var result = {};
44
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
45
+ __setModuleDefault(result, mod);
46
+ return result;
47
+ };
48
+ })();
49
+ Object.defineProperty(exports, "__esModule", { value: true });
50
+ exports.WASM_ENTRY = exports.DEV_DEFAULTS = void 0;
51
+ exports.parseDevArgs = parseDevArgs;
52
+ exports.contentType = contentType;
53
+ exports.wasmBundleCandidates = wasmBundleCandidates;
54
+ exports.renderShell = renderShell;
55
+ const path = __importStar(require("path"));
56
+ exports.DEV_DEFAULTS = {
57
+ resourcesDir: "resources",
58
+ outDir: ".rastack",
59
+ warehouseDir: "data/warehouse",
60
+ port: 4321,
61
+ open: true,
62
+ };
63
+ /**
64
+ * Parse `rastack dev` args: `--port`, `--warehouse`, `--out`, `--no-open`, and a
65
+ * single positional resources dir. Unknown flags are ignored (forgiving CLI).
66
+ */
67
+ function parseDevArgs(argv) {
68
+ const positional = [];
69
+ let { resourcesDir, outDir, warehouseDir, port, open } = exports.DEV_DEFAULTS;
70
+ for (let i = 0; i < argv.length; i++) {
71
+ const a = argv[i];
72
+ if (a === "--port")
73
+ port = Number(argv[++i]) || port;
74
+ else if (a === "--warehouse")
75
+ warehouseDir = argv[++i] ?? warehouseDir;
76
+ else if (a === "--out")
77
+ outDir = argv[++i] ?? outDir;
78
+ else if (a === "--no-open")
79
+ open = false;
80
+ else if (!a.startsWith("-"))
81
+ positional.push(a);
82
+ }
83
+ return {
84
+ resourcesDir: positional[0] || resourcesDir,
85
+ outDir,
86
+ warehouseDir,
87
+ port,
88
+ open,
89
+ };
90
+ }
91
+ const MIME = {
92
+ ".html": "text/html; charset=utf-8",
93
+ ".js": "text/javascript; charset=utf-8",
94
+ ".mjs": "text/javascript; charset=utf-8",
95
+ ".css": "text/css; charset=utf-8",
96
+ ".json": "application/json; charset=utf-8",
97
+ // Critical: `WebAssembly.instantiateStreaming` requires this exact type.
98
+ ".wasm": "application/wasm",
99
+ ".text": "text/plain; charset=utf-8",
100
+ ".parquet": "application/octet-stream",
101
+ };
102
+ /** MIME type for a served path, defaulting to octet-stream (Iceberg data files). */
103
+ function contentType(filePath) {
104
+ const ext = filePath.slice(filePath.lastIndexOf("."));
105
+ return MIME[ext] ?? "application/octet-stream";
106
+ }
107
+ /**
108
+ * Ordered directories to look for the prebuilt wasm bundle (`rastack_wasm.js` +
109
+ * `rastack_wasm_bg.wasm`). The bundle is shipped inside the package
110
+ * (`dist/wasm/`, copied at build), so `rastack dev` works from any repo with no
111
+ * cargo; the repo-local `tools/wasm/` (from a source build) and a sibling of the
112
+ * running module are fallbacks for development. `dirnameDir` is the directory of
113
+ * the compiled `rastack-dev.js` (`dist/`), so `../wasm` and `./wasm` cover both
114
+ * `dist/rastack-dev.js` → `dist/wasm` and `dist/dev/…` layouts.
115
+ */
116
+ function wasmBundleCandidates(dirnameDir, cwd = process.cwd()) {
117
+ return [
118
+ path.join(dirnameDir, "wasm"),
119
+ path.join(dirnameDir, "..", "wasm"),
120
+ path.join(cwd, "tools", "wasm"),
121
+ path.join(cwd, "wasm"),
122
+ ];
123
+ }
124
+ /** The name of the file that marks a directory as a usable wasm bundle. */
125
+ exports.WASM_ENTRY = "rastack_wasm.js";
126
+ /**
127
+ * The HTML shell served at `/`. It loads the bundled React admin
128
+ * (`/dev/admin.js`), which fetches the schema/openapi, boots the wasm engine,
129
+ * seeds it from `/data/warehouse/`, and renders the admin. Deliberately tiny —
130
+ * all logic lives in the bundle — mirroring the `rastack design` studio.
131
+ */
132
+ function renderShell(title = "React API Stack — dev") {
133
+ return `<!doctype html>
134
+ <html lang="en">
135
+ <head>
136
+ <meta charset="utf-8" />
137
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
138
+ <title>${title}</title>
139
+ <style>
140
+ html, body { margin: 0; height: 100%; }
141
+ #root { min-height: 100%; }
142
+ .rastack-boot { font: 14px system-ui, sans-serif; padding: 2rem; color: #555; }
143
+ </style>
144
+ </head>
145
+ <body>
146
+ <div id="root"><div class="rastack-boot">Loading the WASM engine…</div></div>
147
+ <script src="/dev/admin.js"></script>
148
+ </body>
149
+ </html>
150
+ `;
151
+ }
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * `rastack dev` — local development, always over the WASM engine.
4
+ *
5
+ * rastack dev [resourcesDir] [--port <n>] [--warehouse <dir>] [--out <dir>] [--no-open]
6
+ *
7
+ * Compiles the resources, then boots a dependency-free static server (à la
8
+ * `rastack design`) that hands the browser everything it needs to run the app +
9
+ * admin locally over WebAssembly — no native server, no cargo:
10
+ * • the bundled React admin (`/dev/admin.js`),
11
+ * • the prebuilt WASM engine (`/wasm/…`) shipped in the package,
12
+ * • the compiled `schema.rastack.json` / `openapi.json`,
13
+ * • the committed Iceberg warehouse (`/data/warehouse/…`) to seed from.
14
+ *
15
+ * The engine is resolved from the package's prebuilt bundle; if none is present
16
+ * but a Rust + wasm toolchain is, it is built from source as a fallback.
17
+ */
18
+ export {};
@@ -0,0 +1,197 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ /**
4
+ * `rastack dev` — local development, always over the WASM engine.
5
+ *
6
+ * rastack dev [resourcesDir] [--port <n>] [--warehouse <dir>] [--out <dir>] [--no-open]
7
+ *
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`),
12
+ * • the prebuilt WASM engine (`/wasm/…`) shipped in the package,
13
+ * • the compiled `schema.rastack.json` / `openapi.json`,
14
+ * • the committed Iceberg warehouse (`/data/warehouse/…`) to seed from.
15
+ *
16
+ * The engine is resolved from the package's prebuilt bundle; if none is present
17
+ * but a Rust + wasm toolchain is, it is built from source as a fallback.
18
+ */
19
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
20
+ if (k2 === undefined) k2 = k;
21
+ var desc = Object.getOwnPropertyDescriptor(m, k);
22
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
23
+ desc = { enumerable: true, get: function() { return m[k]; } };
24
+ }
25
+ Object.defineProperty(o, k2, desc);
26
+ }) : (function(o, m, k, k2) {
27
+ if (k2 === undefined) k2 = k;
28
+ o[k2] = m[k];
29
+ }));
30
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
31
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
32
+ }) : function(o, v) {
33
+ o["default"] = v;
34
+ });
35
+ var __importStar = (this && this.__importStar) || (function () {
36
+ var ownKeys = function(o) {
37
+ ownKeys = Object.getOwnPropertyNames || function (o) {
38
+ var ar = [];
39
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
40
+ return ar;
41
+ };
42
+ return ownKeys(o);
43
+ };
44
+ return function (mod) {
45
+ if (mod && mod.__esModule) return mod;
46
+ var result = {};
47
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
48
+ __setModuleDefault(result, mod);
49
+ return result;
50
+ };
51
+ })();
52
+ Object.defineProperty(exports, "__esModule", { value: true });
53
+ const child_process_1 = require("child_process");
54
+ const fs = __importStar(require("fs"));
55
+ const http = __importStar(require("http"));
56
+ const path = __importStar(require("path"));
57
+ const harness_1 = require("./dev/harness");
58
+ 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
+ /** True when `cargo` is on PATH (the from-source wasm fallback needs it). */
68
+ function cargoAvailable() {
69
+ try {
70
+ return ((0, child_process_1.spawnSync)("cargo", ["--version"], { stdio: "ignore" }).status === 0);
71
+ }
72
+ catch {
73
+ return false;
74
+ }
75
+ }
76
+ /** Locate the prebuilt wasm bundle dir, building from source as a fallback. */
77
+ function resolveWasmDir() {
78
+ for (const dir of (0, harness_1.wasmBundleCandidates)(HERE)) {
79
+ if (fs.existsSync(path.join(dir, harness_1.WASM_ENTRY)))
80
+ return dir;
81
+ }
82
+ if (cargoAvailable()) {
83
+ console.log("\n▸ No prebuilt WASM engine found — building it from source (cargo)…\n");
84
+ try {
85
+ // Lazy require so a normal `dev` with a prebuilt bundle never loads it.
86
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
87
+ require("./rastack-wasm-build").buildWasm();
88
+ }
89
+ catch (err) {
90
+ console.error(` ✗ wasm build failed: ${err.message}`);
91
+ return null;
92
+ }
93
+ for (const dir of (0, harness_1.wasmBundleCandidates)(HERE)) {
94
+ if (fs.existsSync(path.join(dir, harness_1.WASM_ENTRY)))
95
+ return dir;
96
+ }
97
+ }
98
+ return null;
99
+ }
100
+ /** Serve one file if it exists and stays within `root`; else 404/403. */
101
+ function serveUnder(res, root, relPath) {
102
+ const safeRoot = path.resolve(root);
103
+ const rel = decodeURIComponent(relPath).replace(/^\/+/, "");
104
+ const target = path.resolve(safeRoot, rel);
105
+ // Path-traversal guard: the resolved target must stay inside the root.
106
+ if (target !== safeRoot && !target.startsWith(safeRoot + path.sep)) {
107
+ res.writeHead(403).end("Forbidden");
108
+ return;
109
+ }
110
+ fs.readFile(target, (err, data) => {
111
+ if (err) {
112
+ res.writeHead(404).end("Not found");
113
+ return;
114
+ }
115
+ res.writeHead(200, { "Content-Type": (0, harness_1.contentType)(target) }).end(data);
116
+ });
117
+ }
118
+ /** Serve an exact file (no traversal concern — a fixed internal artifact). */
119
+ function serveFile(res, file) {
120
+ fs.readFile(file, (err, data) => {
121
+ if (err) {
122
+ res.writeHead(404).end("Not found");
123
+ return;
124
+ }
125
+ res.writeHead(200, { "Content-Type": (0, harness_1.contentType)(file) }).end(data);
126
+ });
127
+ }
128
+ /** Best-effort open the default browser at `url`. */
129
+ function openBrowser(url) {
130
+ const cmd = process.platform === "darwin"
131
+ ? "open"
132
+ : process.platform === "win32"
133
+ ? "start"
134
+ : "xdg-open";
135
+ try {
136
+ (0, child_process_1.execFileSync)(cmd, [url], { stdio: "ignore" });
137
+ }
138
+ catch {
139
+ /* non-fatal — the URL is printed anyway */
140
+ }
141
+ }
142
+ function main(argv) {
143
+ const args = (0, harness_1.parseDevArgs)(argv);
144
+ console.log(`\n rastack dev — the app + admin, in your browser over WASM\n`);
145
+ console.log(`▸ Compiling ${args.resourcesDir} → ${args.outDir}`);
146
+ if (!compile(args.resourcesDir, args.outDir)) {
147
+ console.error(" ✗ compile failed — fix the resource graph and re-run.");
148
+ process.exit(1);
149
+ }
150
+ const wasmDir = resolveWasmDir();
151
+ if (!wasmDir) {
152
+ console.error("\n ✗ No WASM engine available.\n" +
153
+ " Upgrade rastack for the prebuilt bundle (`rastack update`), or install\n" +
154
+ " the Rust toolchain to build it from source:\n" +
155
+ " rustup target add wasm32-unknown-unknown\n" +
156
+ " cargo install wasm-bindgen-cli\n");
157
+ process.exit(1);
158
+ }
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
+ const server = http.createServer((req, res) => {
165
+ const url = (req.url || "/").split("?")[0];
166
+ if (req.method !== "GET") {
167
+ res.writeHead(405).end("Method not allowed");
168
+ return;
169
+ }
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);
178
+ if (url.startsWith("/wasm/"))
179
+ return serveUnder(res, wasmDir, url.slice("/wasm/".length));
180
+ if (url === "/schema.rastack.json" || url === "/openapi.json")
181
+ return serveUnder(res, args.outDir, path.basename(url));
182
+ if (url.startsWith("/data/warehouse/"))
183
+ return serveUnder(res, args.warehouseDir, url.slice("/data/warehouse/".length));
184
+ res.writeHead(404).end("Not found");
185
+ });
186
+ server.listen(args.port, () => {
187
+ const url = `http://localhost:${args.port}`;
188
+ 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");
191
+ if (args.open)
192
+ openBrowser(url);
193
+ });
194
+ }
195
+ if (require.main === module) {
196
+ main(process.argv.slice(2));
197
+ }
@@ -11,8 +11,9 @@
11
11
  * 1. writes `.rastack/deploy.json` + `.github/workflows/{ci,deploy-backend}.yml`
12
12
  * (GitHub coordinates default from the `origin` remote; missing fields are
13
13
  * prompted for on a TTY);
14
- * 2. resolves the tier-0 bootstrap stack's artifacts bucket — and CREATES the
15
- * bootstrap stack if it does not exist yet;
14
+ * 2. resolves the tier-0 bootstrap stack's artifacts bucket — erroring with a
15
+ * pointer to `rastack bootstrap` when that account+region stack is missing
16
+ * (init is per-app and never creates account-level infrastructure);
16
17
  * 3. uploads the Lambda placeholder bundle to that bucket;
17
18
  * 4. deploys the per-app backend (app.yaml) with the resolved ArtifactsBucket;
18
19
  * 5. reads the stack Outputs back into the config and prints the two GitHub repo
@@ -12,8 +12,9 @@
12
12
  * 1. writes `.rastack/deploy.json` + `.github/workflows/{ci,deploy-backend}.yml`
13
13
  * (GitHub coordinates default from the `origin` remote; missing fields are
14
14
  * prompted for on a TTY);
15
- * 2. resolves the tier-0 bootstrap stack's artifacts bucket — and CREATES the
16
- * bootstrap stack if it does not exist yet;
15
+ * 2. resolves the tier-0 bootstrap stack's artifacts bucket — erroring with a
16
+ * pointer to `rastack bootstrap` when that account+region stack is missing
17
+ * (init is per-app and never creates account-level infrastructure);
17
18
  * 3. uploads the Lambda placeholder bundle to that bucket;
18
19
  * 4. deploys the per-app backend (app.yaml) with the resolved ArtifactsBucket;
19
20
  * 5. reads the stack Outputs back into the config and prints the two GitHub repo
@@ -187,14 +188,13 @@ function printPlan(cfg) {
187
188
  const withBucket = (0, config_1.mergeConfig)(cfg, {
188
189
  artifactsBucket: bucket ?? "<ArtifactsBucketName from bootstrap stack>",
189
190
  });
190
- const bootstrapTemplate = (0, io_1.resolveDeployAsset)("bootstrap.yaml") ?? undefined;
191
191
  const appTemplate = (0, io_1.resolveDeployAsset)("app.yaml") ?? undefined;
192
192
  console.log(`\n Deployment plan (${cfg.stackName} @ ${cfg.region})\n`);
193
- console.log(` ── 1. Bootstrap stack (${(0, config_1.bootstrapStackName)(cfg)}) — run only if it does not exist ──`);
194
- console.log("\n " + (0, config_1.bootstrapStackCommand)(cfg, false, bootstrapTemplate) + "\n");
195
- console.log(" ── 2. Per-app backend (app.yaml) ──");
193
+ console.log(` ── Prerequisite: the tier-0 bootstrap stack (${(0, config_1.bootstrapStackName)(cfg)}) must exist ──`);
194
+ console.log(` Create it once per account+region (init does not): rastack bootstrap --region ${cfg.region}\n`);
195
+ console.log(" ── 1. Per-app backend (app.yaml) — what init deploys ──");
196
196
  console.log("\n " + (0, config_1.deployStackCommand)(withBucket, appTemplate) + "\n");
197
- console.log(" ── 3. Wire these GitHub repo Variables from the stack Outputs ──");
197
+ console.log(" ── 2. Wire these GitHub repo Variables from the stack Outputs ──");
198
198
  console.log(table((0, config_1.repoVariables)(cfg).map((v) => ({
199
199
  key: v.name,
200
200
  value: v.value || `<${v.from}>`,
@@ -211,18 +211,22 @@ async function standUp(cfg, interactive) {
211
211
  " or re-run with --print to see the commands, or --no-deploy to skip.");
212
212
  process.exit(1);
213
213
  }
214
- // 1. Resolve the artifacts bucket; bootstrap the tier-0 stack if it is absent.
215
- let artifactsBucket = (0, io_1.stackOutput)((0, config_1.bootstrapStackName)(cfg), config_1.BOOTSTRAP_ARTIFACTS_OUTPUT, cfg.region);
216
- let templatesBucket = (0, io_1.stackOutput)((0, config_1.bootstrapStackName)(cfg), config_1.BOOTSTRAP_TEMPLATES_OUTPUT, cfg.region);
214
+ // 1. Resolve the artifacts bucket from the tier-0 bootstrap stack. That stack
215
+ // is an account+region once-off — the S3 buckets that host the templates +
216
+ // Lambda placeholder, and the GitHub OIDC provider. `init` is a per-app
217
+ // command and deliberately does NOT create it: standing up account-level
218
+ // infrastructure is `rastack bootstrap`'s job (run once by an account admin),
219
+ // so a missing bootstrap stack is a clear, actionable error, not an implicit
220
+ // account mutation buried inside an app deploy.
221
+ const artifactsBucket = (0, io_1.stackOutput)((0, config_1.bootstrapStackName)(cfg), config_1.BOOTSTRAP_ARTIFACTS_OUTPUT, cfg.region);
222
+ const templatesBucket = (0, io_1.stackOutput)((0, config_1.bootstrapStackName)(cfg), config_1.BOOTSTRAP_TEMPLATES_OUTPUT, cfg.region);
217
223
  if (!artifactsBucket) {
218
- console.log(`\n▸ Bootstrap stack ${(0, config_1.bootstrapStackName)(cfg)} not found — creating it (once per account+region).`);
219
- deployBootstrap(cfg);
220
- artifactsBucket = (0, io_1.stackOutput)((0, config_1.bootstrapStackName)(cfg), config_1.BOOTSTRAP_ARTIFACTS_OUTPUT, cfg.region);
221
- templatesBucket = (0, io_1.stackOutput)((0, config_1.bootstrapStackName)(cfg), config_1.BOOTSTRAP_TEMPLATES_OUTPUT, cfg.region);
222
- if (!artifactsBucket) {
223
- console.error(" ✗ bootstrap stack created but its ArtifactsBucket output could not be read.");
224
- process.exit(1);
225
- }
224
+ console.error(`\n ✗ Bootstrap stack ${(0, config_1.bootstrapStackName)(cfg)} not found in ${cfg.region}.\n` +
225
+ " It is the once-per-account+region step that hosts the CloudFormation\n" +
226
+ " templates and the Lambda placeholder every app deploy reads. Create it\n" +
227
+ " first (an account admin does this once), then re-run `rastack init`:\n\n" +
228
+ ` rastack bootstrap --region ${cfg.region}\n`);
229
+ process.exit(1);
226
230
  }
227
231
  console.log(` ✓ artifacts bucket: ${artifactsBucket}`);
228
232
  // 2. Upload the placeholder the function is created from.
@@ -305,31 +309,6 @@ function wireRepoVariables(cfg, interactive) {
305
309
  }
306
310
  }
307
311
  }
308
- /** Deploy the bootstrap stack, retrying once against an existing OIDC provider. */
309
- function deployBootstrap(cfg) {
310
- const template = (0, io_1.resolveDeployAsset)("bootstrap.yaml");
311
- if (!template) {
312
- console.error(" ✗ bootstrap.yaml template not found in the rastack package. Reinstall\n" +
313
- " rastack (`npm i -g rastack`) or run from the framework repo.");
314
- process.exit(1);
315
- }
316
- try {
317
- (0, io_1.awsRun)((0, config_1.bootstrapStackArgs)(cfg, false, template));
318
- }
319
- catch {
320
- // The most common cause is an account that already has a GitHub OIDC
321
- // provider (only one is allowed per URL). Retry telling the template to
322
- // reuse it instead of creating a second.
323
- console.log(" • retrying bootstrap, reusing the account's existing GitHub OIDC provider");
324
- try {
325
- (0, io_1.awsRun)((0, config_1.bootstrapStackArgs)(cfg, true, template));
326
- }
327
- catch (err) {
328
- console.error(`\n ✗ bootstrap stack deploy failed.\n ${err.message}`);
329
- process.exit(1);
330
- }
331
- }
332
- }
333
312
  function printSummary(cfg) {
334
313
  console.log(`\n ✓ ${cfg.app} is deployed (${cfg.stackName} @ ${cfg.region})`);
335
314
  if (cfg.outputs?.apiUrl)
@@ -399,7 +378,7 @@ async function main(argv) {
399
378
  return;
400
379
  }
401
380
  if ((0, io_1.canPrompt)()) {
402
- const ok = await confirm(`Deploy ${cfg.stackName} to AWS ${cfg.region} now? (creates the bootstrap stack if needed) [y/N]`);
381
+ const ok = await confirm(`Deploy ${cfg.stackName} to AWS ${cfg.region} now? (requires the bootstrap stack to exist) [y/N]`);
403
382
  if (!ok) {
404
383
  console.log("\n Skipped the deploy. Re-run to deploy, or `rastack init --print` for the commands.\n");
405
384
  return;
@@ -74,15 +74,24 @@ function readVersion(file) {
74
74
  return "unknown";
75
75
  }
76
76
  }
77
+ /**
78
+ * Run npm and return stdout, or throw on failure. Windows must launch the
79
+ * `npm.cmd` shim through a shell; we pass one command string to `execSync`
80
+ * rather than an args array with `shell: true` (Node 22+ DEP0190 — args would be
81
+ * concatenated unescaped). The only non-literal token, the version, is already
82
+ * sanitized (`sanitizeVersion`). POSIX keeps the shell-free `execFile`.
83
+ */
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);
89
+ }
77
90
  /** Capture `npm <args>` stdout, or null when npm is absent / the call fails. */
78
91
  function npmCapture(args) {
79
92
  try {
80
- return (0, child_process_1.execFileSync)(NPM, args, {
81
- stdio: ["ignore", "pipe", "pipe"],
82
- shell: NPM_SHELL,
83
- })
84
- .toString()
85
- .trim();
93
+ const out = runNpm(args, { stdio: ["ignore", "pipe", "pipe"] });
94
+ return (out ?? "").toString().trim();
86
95
  }
87
96
  catch {
88
97
  return null;
@@ -129,8 +138,7 @@ function main(argv) {
129
138
  // the `.cmd` shim is launched through a shell (EINVAL otherwise); the only
130
139
  // caller-supplied token — the version — is sanitized in `parseUpdateFlags`,
131
140
  // and every other arg is a fixed literal, so nothing injectable reaches it.
132
- // nosemgrep: javascript.lang.security.detect-child-process.detect-child-process
133
- (0, child_process_1.execFileSync)(NPM, args, { stdio: "inherit", env, shell: NPM_SHELL });
141
+ runNpm(args, { stdio: "inherit", env });
134
142
  }
135
143
  catch (err) {
136
144
  console.error(`\n ✗ npm failed to update rastack. Re-run for detail:\n npm install -g ${(0, update_1.npmInstallArgs)(flags.version)[2]}\n ${err.message}`);
@@ -1,14 +1,19 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * `rastack wasm` — compile the Rust API to WebAssembly for in-browser local dev.
3
+ * Compile the Rust API to WebAssembly for in-browser local dev — the engine
4
+ * `rastack dev` serves and `<RAStackProvider mode="local">` loads.
4
5
  *
5
6
  * Pipeline:
6
7
  * 1. cargo build -p rastack-wasm --release --target wasm32-unknown-unknown
7
- * 2. wasm-bindgen --target web → stack/wasm/rastack_wasm.js + rastack_wasm_bg.wasm
8
+ * 2. wasm-bindgen --target web → tools/wasm/rastack_wasm.js + rastack_wasm_bg.wasm
8
9
  * 3. wasm-opt -Oz (if available) to shrink the binary
9
10
  *
10
- * The generated bundle is what `<RAStackProvider mode="local">` loads. Prereqs:
11
+ * This is invoked by the release CI (to bundle a prebuilt engine into the npm
12
+ * package) and by `rastack dev` as a from-source fallback when no prebuilt
13
+ * bundle is present but the toolchain is. Prereqs:
11
14
  * rustup target add wasm32-unknown-unknown
12
15
  * cargo install wasm-bindgen-cli (version must match the wasm-bindgen crate)
13
16
  */
14
- export {};
17
+ /** Run the full compile → wasm-bindgen → optimise pipeline. Throws via `run`'s
18
+ * `process.exit` on a missing tool or a non-zero step. */
19
+ export declare function buildWasm(): void;