rastack 0.0.43 → 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.
- package/CHANGELOG.md +7 -0
- package/admin/AdminApp.tsx +308 -0
- package/admin/adapters.ts +247 -0
- package/admin/index.tsx +88 -0
- package/admin/useAdmin.ts +299 -0
- package/dist/admin.js +81 -0
- package/dist/dev/harness.d.ts +53 -0
- package/dist/dev/harness.js +151 -0
- package/dist/rastack-dev.d.ts +18 -0
- package/dist/rastack-dev.js +197 -0
- package/dist/rastack-wasm-build.d.ts +9 -4
- package/dist/rastack-wasm-build.js +44 -33
- package/dist/rastack.d.ts +1 -2
- package/dist/rastack.js +7 -10
- package/dist/wasm/package.json +4 -0
- package/dist/wasm/rastack_wasm.d.ts +100 -0
- package/dist/wasm/rastack_wasm.js +745 -0
- package/dist/wasm/rastack_wasm_bg.wasm +0 -0
- package/dist/wasm/rastack_wasm_bg.wasm.d.ts +28 -0
- package/package.json +6 -2
- package/scripts/build-admin.js +48 -0
- package/scripts/copy-wasm.js +35 -0
- package/src/dev/harness.ts +130 -0
- package/src/rastack-dev.ts +197 -0
- package/src/rastack-wasm-build.ts +52 -41
- package/src/rastack.ts +7 -10
- package/test/dev.spec.ts +277 -0
- package/wasm/package.json +4 -0
- package/wasm/rastack_wasm.d.ts +100 -0
- package/wasm/rastack_wasm.js +745 -0
- package/wasm/rastack_wasm_bg.wasm +0 -0
- package/wasm/rastack_wasm_bg.wasm.d.ts +28 -0
|
Binary file
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/* tslint:disable */
|
|
2
|
+
/* eslint-disable */
|
|
3
|
+
export const memory: WebAssembly.Memory;
|
|
4
|
+
export const __wbg_rastackapi_free: (a: number, b: number) => void;
|
|
5
|
+
export const rastackapi_exportWarehouse: (a: number) => any;
|
|
6
|
+
export const rastackapi_handle: (a: number, b: number, c: number, d: number, e: number, f: any, g: any) => any;
|
|
7
|
+
export const rastackapi_importWarehouse: (a: number, b: number, c: number) => any;
|
|
8
|
+
export const rastackapi_new: (a: number, b: number, c: number, d: number) => [number, number, number];
|
|
9
|
+
export const rastackapi_seedFile: (a: number, b: number, c: number, d: number, e: number) => any;
|
|
10
|
+
export const rastackapi_setAuth: (a: number, b: number, c: number, d: any) => [number, number];
|
|
11
|
+
export const rust_zstd_wasm_shim_calloc: (a: number, b: number) => number;
|
|
12
|
+
export const rust_zstd_wasm_shim_free: (a: number) => void;
|
|
13
|
+
export const rust_zstd_wasm_shim_malloc: (a: number) => number;
|
|
14
|
+
export const rust_zstd_wasm_shim_memcmp: (a: number, b: number, c: number) => number;
|
|
15
|
+
export const rust_zstd_wasm_shim_memcpy: (a: number, b: number, c: number) => number;
|
|
16
|
+
export const rust_zstd_wasm_shim_memmove: (a: number, b: number, c: number) => number;
|
|
17
|
+
export const rust_zstd_wasm_shim_memset: (a: number, b: number, c: number) => number;
|
|
18
|
+
export const rust_zstd_wasm_shim_qsort: (a: number, b: number, c: number, d: number) => void;
|
|
19
|
+
export const wasm_bindgen_1a7aa9db5392c486___convert__closures_____invoke___wasm_bindgen_1a7aa9db5392c486___JsValue__core_7d5f0a2ba6a62c33___result__Result_____wasm_bindgen_1a7aa9db5392c486___JsError___true_: (a: number, b: number, c: any) => [number, number];
|
|
20
|
+
export const wasm_bindgen_1a7aa9db5392c486___convert__closures_____invoke___js_sys_191ff9fb2bd47800___Function_fn_wasm_bindgen_1a7aa9db5392c486___JsValue_____wasm_bindgen_1a7aa9db5392c486___sys__Undefined___js_sys_191ff9fb2bd47800___Function_fn_wasm_bindgen_1a7aa9db5392c486___JsValue_____wasm_bindgen_1a7aa9db5392c486___sys__Undefined_______true_: (a: number, b: number, c: any, d: any) => void;
|
|
21
|
+
export const __wbindgen_malloc: (a: number, b: number) => number;
|
|
22
|
+
export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
|
|
23
|
+
export const __wbindgen_exn_store: (a: number) => void;
|
|
24
|
+
export const __externref_table_alloc: () => number;
|
|
25
|
+
export const __wbindgen_externrefs: WebAssembly.Table;
|
|
26
|
+
export const __wbindgen_destroy_closure: (a: number, b: number) => void;
|
|
27
|
+
export const __externref_table_dealloc: (a: number) => void;
|
|
28
|
+
export const __wbindgen_start: () => void;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rastack",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.44",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "runtime.ts",
|
|
6
6
|
"types": "runtime.ts",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
},
|
|
14
14
|
"scripts": {
|
|
15
15
|
"format": "prettier --write \"**/*.{ts,tsx,md}\"",
|
|
16
|
-
"build": "tsc && cp src/.prettierrc dist/.prettierrc && node scripts/copy-templates.js",
|
|
16
|
+
"build": "tsc && cp src/.prettierrc dist/.prettierrc && node scripts/copy-templates.js && node scripts/build-admin.js && node scripts/copy-wasm.js",
|
|
17
17
|
"start": "node dist/index.js",
|
|
18
18
|
"test": "jest"
|
|
19
19
|
},
|
|
@@ -50,8 +50,12 @@
|
|
|
50
50
|
"@types/js-yaml": "^4.0.9",
|
|
51
51
|
"@types/mocha": "^10.0.6",
|
|
52
52
|
"@types/node": "^26.1.0",
|
|
53
|
+
"@types/react-dom": "^19.2.0",
|
|
54
|
+
"esbuild": "^0.25.0",
|
|
53
55
|
"eslint": "^10.6.0",
|
|
54
56
|
"jest": "^30.4.2",
|
|
57
|
+
"react": "^19.2.0",
|
|
58
|
+
"react-dom": "^19.2.0",
|
|
55
59
|
"ts-jest": "^29.4.11"
|
|
56
60
|
}
|
|
57
61
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Bundle the in-browser admin (`admin/index.tsx` + the reused `rastack/components`
|
|
4
|
+
* / `rastack/theme` / `rastack/provider` React source) into a single static
|
|
5
|
+
* `dist/admin.js` that `rastack dev` serves at `/dev/admin.js`.
|
|
6
|
+
*
|
|
7
|
+
* `tools/` otherwise builds with plain `tsc`, and the React runtime dirs ship as
|
|
8
|
+
* `.tsx` source (never compiled), so a bundler is required to turn the admin
|
|
9
|
+
* entrypoint into a browser asset. esbuild is that bundler. The bare
|
|
10
|
+
* `rastack/tokens` + `rastack/theme` subpath imports the components use are
|
|
11
|
+
* aliased to the in-repo sources (mirroring tsconfig `paths` / jest
|
|
12
|
+
* `moduleNameMapper`). The WASM engine is loaded at runtime via a hidden
|
|
13
|
+
* `import()` (see admin/index.tsx), so esbuild never tries to resolve it.
|
|
14
|
+
*/
|
|
15
|
+
const path = require("path");
|
|
16
|
+
const esbuild = require("esbuild");
|
|
17
|
+
|
|
18
|
+
const root = path.join(__dirname, "..");
|
|
19
|
+
|
|
20
|
+
esbuild
|
|
21
|
+
.build({
|
|
22
|
+
entryPoints: [path.join(root, "admin", "index.tsx")],
|
|
23
|
+
bundle: true,
|
|
24
|
+
outfile: path.join(root, "dist", "admin.js"),
|
|
25
|
+
format: "iife",
|
|
26
|
+
platform: "browser",
|
|
27
|
+
target: ["es2020"],
|
|
28
|
+
minify: true,
|
|
29
|
+
sourcemap: false,
|
|
30
|
+
jsx: "automatic",
|
|
31
|
+
// The reused provider/hooks read `process.env.*` (e.g. NEXT_PUBLIC_BASE_URL).
|
|
32
|
+
// `define` folds NODE_ENV; the banner shims a `process.env` object so any
|
|
33
|
+
// other lookups resolve to undefined instead of throwing in the browser.
|
|
34
|
+
define: { "process.env.NODE_ENV": '"production"' },
|
|
35
|
+
banner: {
|
|
36
|
+
js: "window.process=window.process||{env:{}};",
|
|
37
|
+
},
|
|
38
|
+
alias: {
|
|
39
|
+
"rastack/tokens": path.join(root, "tokens.ts"),
|
|
40
|
+
"rastack/theme": path.join(root, "theme", "index.ts"),
|
|
41
|
+
},
|
|
42
|
+
logLevel: "info",
|
|
43
|
+
})
|
|
44
|
+
.then(() => console.log("build-admin: wrote dist/admin.js"))
|
|
45
|
+
.catch((err) => {
|
|
46
|
+
console.error("build-admin: failed\n", err);
|
|
47
|
+
process.exit(1);
|
|
48
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Copy the prebuilt WASM engine (`tools/wasm/`, produced by
|
|
4
|
+
* `rastack-wasm-build.js`) into the published package at `dist/wasm/`, so
|
|
5
|
+
* `rastack dev` serves it from any repo with no cargo. A no-op when no bundle
|
|
6
|
+
* has been built yet (a normal `pnpm build` on a machine without the Rust
|
|
7
|
+
* toolchain), so ordinary builds still succeed — the release CI builds the
|
|
8
|
+
* engine before this runs, and `rastack dev` has a from-source fallback.
|
|
9
|
+
*/
|
|
10
|
+
const fs = require("fs");
|
|
11
|
+
const path = require("path");
|
|
12
|
+
|
|
13
|
+
const root = path.join(__dirname, "..");
|
|
14
|
+
const src = path.join(root, "wasm");
|
|
15
|
+
const dest = path.join(root, "dist", "wasm");
|
|
16
|
+
const entry = path.join(src, "rastack_wasm.js");
|
|
17
|
+
|
|
18
|
+
if (!fs.existsSync(entry)) {
|
|
19
|
+
console.log(
|
|
20
|
+
"copy-wasm: no prebuilt engine at tools/wasm (skipping — build it with " +
|
|
21
|
+
"rastack-wasm-build.js to ship it).",
|
|
22
|
+
);
|
|
23
|
+
process.exit(0);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
27
|
+
let n = 0;
|
|
28
|
+
for (const name of fs.readdirSync(src)) {
|
|
29
|
+
const from = path.join(src, name);
|
|
30
|
+
if (fs.statSync(from).isFile()) {
|
|
31
|
+
fs.copyFileSync(from, path.join(dest, name));
|
|
32
|
+
n++;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
console.log(`copy-wasm: copied ${n} file(s) into dist/wasm`);
|
|
@@ -0,0 +1,130 @@
|
|
|
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
|
+
|
|
16
|
+
import * as path from "path";
|
|
17
|
+
|
|
18
|
+
export interface DevArgs {
|
|
19
|
+
/** Where the TypeScript resource definitions live (compiled on start). */
|
|
20
|
+
resourcesDir: string;
|
|
21
|
+
/** Where `rastack compile` writes schema.rastack.json + openapi.json. */
|
|
22
|
+
outDir: string;
|
|
23
|
+
/** The committed Iceberg warehouse served to seed the browser engine. */
|
|
24
|
+
warehouseDir: string;
|
|
25
|
+
/** Port the dev server listens on. */
|
|
26
|
+
port: number;
|
|
27
|
+
/** Open the browser on start (false with `--no-open`). */
|
|
28
|
+
open: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const DEV_DEFAULTS: DevArgs = {
|
|
32
|
+
resourcesDir: "resources",
|
|
33
|
+
outDir: ".rastack",
|
|
34
|
+
warehouseDir: "data/warehouse",
|
|
35
|
+
port: 4321,
|
|
36
|
+
open: true,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Parse `rastack dev` args: `--port`, `--warehouse`, `--out`, `--no-open`, and a
|
|
41
|
+
* single positional resources dir. Unknown flags are ignored (forgiving CLI).
|
|
42
|
+
*/
|
|
43
|
+
export function parseDevArgs(argv: string[]): DevArgs {
|
|
44
|
+
const positional: string[] = [];
|
|
45
|
+
let { resourcesDir, outDir, warehouseDir, port, open } = DEV_DEFAULTS;
|
|
46
|
+
for (let i = 0; i < argv.length; i++) {
|
|
47
|
+
const a = argv[i];
|
|
48
|
+
if (a === "--port") port = Number(argv[++i]) || port;
|
|
49
|
+
else if (a === "--warehouse") warehouseDir = argv[++i] ?? warehouseDir;
|
|
50
|
+
else if (a === "--out") outDir = argv[++i] ?? outDir;
|
|
51
|
+
else if (a === "--no-open") open = false;
|
|
52
|
+
else if (!a.startsWith("-")) positional.push(a);
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
resourcesDir: positional[0] || resourcesDir,
|
|
56
|
+
outDir,
|
|
57
|
+
warehouseDir,
|
|
58
|
+
port,
|
|
59
|
+
open,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const MIME: Record<string, string> = {
|
|
64
|
+
".html": "text/html; charset=utf-8",
|
|
65
|
+
".js": "text/javascript; charset=utf-8",
|
|
66
|
+
".mjs": "text/javascript; charset=utf-8",
|
|
67
|
+
".css": "text/css; charset=utf-8",
|
|
68
|
+
".json": "application/json; charset=utf-8",
|
|
69
|
+
// Critical: `WebAssembly.instantiateStreaming` requires this exact type.
|
|
70
|
+
".wasm": "application/wasm",
|
|
71
|
+
".text": "text/plain; charset=utf-8",
|
|
72
|
+
".parquet": "application/octet-stream",
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/** MIME type for a served path, defaulting to octet-stream (Iceberg data files). */
|
|
76
|
+
export function contentType(filePath: string): string {
|
|
77
|
+
const ext = filePath.slice(filePath.lastIndexOf("."));
|
|
78
|
+
return MIME[ext] ?? "application/octet-stream";
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Ordered directories to look for the prebuilt wasm bundle (`rastack_wasm.js` +
|
|
83
|
+
* `rastack_wasm_bg.wasm`). The bundle is shipped inside the package
|
|
84
|
+
* (`dist/wasm/`, copied at build), so `rastack dev` works from any repo with no
|
|
85
|
+
* cargo; the repo-local `tools/wasm/` (from a source build) and a sibling of the
|
|
86
|
+
* running module are fallbacks for development. `dirnameDir` is the directory of
|
|
87
|
+
* the compiled `rastack-dev.js` (`dist/`), so `../wasm` and `./wasm` cover both
|
|
88
|
+
* `dist/rastack-dev.js` → `dist/wasm` and `dist/dev/…` layouts.
|
|
89
|
+
*/
|
|
90
|
+
export function wasmBundleCandidates(
|
|
91
|
+
dirnameDir: string,
|
|
92
|
+
cwd = process.cwd(),
|
|
93
|
+
): string[] {
|
|
94
|
+
return [
|
|
95
|
+
path.join(dirnameDir, "wasm"),
|
|
96
|
+
path.join(dirnameDir, "..", "wasm"),
|
|
97
|
+
path.join(cwd, "tools", "wasm"),
|
|
98
|
+
path.join(cwd, "wasm"),
|
|
99
|
+
];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** The name of the file that marks a directory as a usable wasm bundle. */
|
|
103
|
+
export const WASM_ENTRY = "rastack_wasm.js";
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* The HTML shell served at `/`. It loads the bundled React admin
|
|
107
|
+
* (`/dev/admin.js`), which fetches the schema/openapi, boots the wasm engine,
|
|
108
|
+
* seeds it from `/data/warehouse/`, and renders the admin. Deliberately tiny —
|
|
109
|
+
* all logic lives in the bundle — mirroring the `rastack design` studio.
|
|
110
|
+
*/
|
|
111
|
+
export function renderShell(title = "React API Stack — dev"): string {
|
|
112
|
+
return `<!doctype html>
|
|
113
|
+
<html lang="en">
|
|
114
|
+
<head>
|
|
115
|
+
<meta charset="utf-8" />
|
|
116
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
117
|
+
<title>${title}</title>
|
|
118
|
+
<style>
|
|
119
|
+
html, body { margin: 0; height: 100%; }
|
|
120
|
+
#root { min-height: 100%; }
|
|
121
|
+
.rastack-boot { font: 14px system-ui, sans-serif; padding: 2rem; color: #555; }
|
|
122
|
+
</style>
|
|
123
|
+
</head>
|
|
124
|
+
<body>
|
|
125
|
+
<div id="root"><div class="rastack-boot">Loading the WASM engine…</div></div>
|
|
126
|
+
<script src="/dev/admin.js"></script>
|
|
127
|
+
</body>
|
|
128
|
+
</html>
|
|
129
|
+
`;
|
|
130
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
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
|
+
|
|
20
|
+
import { execFileSync, spawnSync } from "child_process";
|
|
21
|
+
import * as fs from "fs";
|
|
22
|
+
import * as http from "http";
|
|
23
|
+
import * as path from "path";
|
|
24
|
+
import {
|
|
25
|
+
WASM_ENTRY,
|
|
26
|
+
contentType,
|
|
27
|
+
parseDevArgs,
|
|
28
|
+
renderShell,
|
|
29
|
+
wasmBundleCandidates,
|
|
30
|
+
} from "./dev/harness";
|
|
31
|
+
|
|
32
|
+
const HERE = __dirname; // dist/ when installed
|
|
33
|
+
|
|
34
|
+
/** Compile the resources to schema.rastack.json + openapi.json in outDir. */
|
|
35
|
+
function compile(resourcesDir: string, outDir: string): boolean {
|
|
36
|
+
const compiler = path.join(HERE, "rastack-compile.js");
|
|
37
|
+
const result = spawnSync(
|
|
38
|
+
process.execPath,
|
|
39
|
+
[compiler, "compile", resourcesDir, outDir],
|
|
40
|
+
{ stdio: "inherit" },
|
|
41
|
+
);
|
|
42
|
+
if (result.status !== 0) return false;
|
|
43
|
+
return fs.existsSync(path.join(outDir, "schema.rastack.json"));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** True when `cargo` is on PATH (the from-source wasm fallback needs it). */
|
|
47
|
+
function cargoAvailable(): boolean {
|
|
48
|
+
try {
|
|
49
|
+
return (
|
|
50
|
+
spawnSync("cargo", ["--version"], { stdio: "ignore" }).status === 0
|
|
51
|
+
);
|
|
52
|
+
} catch {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Locate the prebuilt wasm bundle dir, building from source as a fallback. */
|
|
58
|
+
function resolveWasmDir(): string | null {
|
|
59
|
+
for (const dir of wasmBundleCandidates(HERE)) {
|
|
60
|
+
if (fs.existsSync(path.join(dir, WASM_ENTRY))) return dir;
|
|
61
|
+
}
|
|
62
|
+
if (cargoAvailable()) {
|
|
63
|
+
console.log(
|
|
64
|
+
"\n▸ No prebuilt WASM engine found — building it from source (cargo)…\n",
|
|
65
|
+
);
|
|
66
|
+
try {
|
|
67
|
+
// Lazy require so a normal `dev` with a prebuilt bundle never loads it.
|
|
68
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
69
|
+
require("./rastack-wasm-build").buildWasm();
|
|
70
|
+
} catch (err) {
|
|
71
|
+
console.error(` ✗ wasm build failed: ${(err as Error).message}`);
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
for (const dir of wasmBundleCandidates(HERE)) {
|
|
75
|
+
if (fs.existsSync(path.join(dir, WASM_ENTRY))) return dir;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Serve one file if it exists and stays within `root`; else 404/403. */
|
|
82
|
+
function serveUnder(
|
|
83
|
+
res: http.ServerResponse,
|
|
84
|
+
root: string,
|
|
85
|
+
relPath: string,
|
|
86
|
+
): void {
|
|
87
|
+
const safeRoot = path.resolve(root);
|
|
88
|
+
const rel = decodeURIComponent(relPath).replace(/^\/+/, "");
|
|
89
|
+
const target = path.resolve(safeRoot, rel);
|
|
90
|
+
// Path-traversal guard: the resolved target must stay inside the root.
|
|
91
|
+
if (target !== safeRoot && !target.startsWith(safeRoot + path.sep)) {
|
|
92
|
+
res.writeHead(403).end("Forbidden");
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
fs.readFile(target, (err, data) => {
|
|
96
|
+
if (err) {
|
|
97
|
+
res.writeHead(404).end("Not found");
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
res.writeHead(200, { "Content-Type": contentType(target) }).end(data);
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Serve an exact file (no traversal concern — a fixed internal artifact). */
|
|
105
|
+
function serveFile(res: http.ServerResponse, file: string): void {
|
|
106
|
+
fs.readFile(file, (err, data) => {
|
|
107
|
+
if (err) {
|
|
108
|
+
res.writeHead(404).end("Not found");
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
res.writeHead(200, { "Content-Type": contentType(file) }).end(data);
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Best-effort open the default browser at `url`. */
|
|
116
|
+
function openBrowser(url: string): void {
|
|
117
|
+
const cmd =
|
|
118
|
+
process.platform === "darwin"
|
|
119
|
+
? "open"
|
|
120
|
+
: process.platform === "win32"
|
|
121
|
+
? "start"
|
|
122
|
+
: "xdg-open";
|
|
123
|
+
try {
|
|
124
|
+
execFileSync(cmd, [url], { stdio: "ignore" });
|
|
125
|
+
} catch {
|
|
126
|
+
/* non-fatal — the URL is printed anyway */
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function main(argv: string[]): void {
|
|
131
|
+
const args = parseDevArgs(argv);
|
|
132
|
+
|
|
133
|
+
console.log(`\n rastack dev — the app + admin, in your browser over WASM\n`);
|
|
134
|
+
console.log(`▸ Compiling ${args.resourcesDir} → ${args.outDir}`);
|
|
135
|
+
if (!compile(args.resourcesDir, args.outDir)) {
|
|
136
|
+
console.error(" ✗ compile failed — fix the resource graph and re-run.");
|
|
137
|
+
process.exit(1);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const wasmDir = resolveWasmDir();
|
|
141
|
+
if (!wasmDir) {
|
|
142
|
+
console.error(
|
|
143
|
+
"\n ✗ No WASM engine available.\n" +
|
|
144
|
+
" Upgrade rastack for the prebuilt bundle (`rastack update`), or install\n" +
|
|
145
|
+
" the Rust toolchain to build it from source:\n" +
|
|
146
|
+
" rustup target add wasm32-unknown-unknown\n" +
|
|
147
|
+
" cargo install wasm-bindgen-cli\n",
|
|
148
|
+
);
|
|
149
|
+
process.exit(1);
|
|
150
|
+
}
|
|
151
|
+
|
|
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
|
+
}
|
|
159
|
+
|
|
160
|
+
const server = http.createServer((req, res) => {
|
|
161
|
+
const url = (req.url || "/").split("?")[0];
|
|
162
|
+
if (req.method !== "GET") {
|
|
163
|
+
res.writeHead(405).end("Method not allowed");
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
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);
|
|
173
|
+
if (url.startsWith("/wasm/"))
|
|
174
|
+
return serveUnder(res, wasmDir, url.slice("/wasm/".length));
|
|
175
|
+
if (url === "/schema.rastack.json" || url === "/openapi.json")
|
|
176
|
+
return serveUnder(res, args.outDir, path.basename(url));
|
|
177
|
+
if (url.startsWith("/data/warehouse/"))
|
|
178
|
+
return serveUnder(
|
|
179
|
+
res,
|
|
180
|
+
args.warehouseDir,
|
|
181
|
+
url.slice("/data/warehouse/".length),
|
|
182
|
+
);
|
|
183
|
+
res.writeHead(404).end("Not found");
|
|
184
|
+
});
|
|
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) openBrowser(url);
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (require.main === module) {
|
|
196
|
+
main(process.argv.slice(2));
|
|
197
|
+
}
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* Compile the Rust API to WebAssembly for in-browser local dev — the engine
|
|
5
|
+
* `rastack dev` serves and `<RAStackProvider mode="local">` loads.
|
|
5
6
|
*
|
|
6
7
|
* Pipeline:
|
|
7
8
|
* 1. cargo build -p rastack-wasm --release --target wasm32-unknown-unknown
|
|
8
|
-
* 2. wasm-bindgen --target web →
|
|
9
|
+
* 2. wasm-bindgen --target web → tools/wasm/rastack_wasm.js + rastack_wasm_bg.wasm
|
|
9
10
|
* 3. wasm-opt -Oz (if available) to shrink the binary
|
|
10
11
|
*
|
|
11
|
-
*
|
|
12
|
+
* This is invoked by the release CI (to bundle a prebuilt engine into the npm
|
|
13
|
+
* package) and by `rastack dev` as a from-source fallback when no prebuilt
|
|
14
|
+
* bundle is present but the toolchain is. Prereqs:
|
|
12
15
|
* rustup target add wasm32-unknown-unknown
|
|
13
16
|
* cargo install wasm-bindgen-cli (version must match the wasm-bindgen crate)
|
|
14
17
|
*/
|
|
@@ -56,47 +59,55 @@ function has(cmd: string): boolean {
|
|
|
56
59
|
return spawnSync(cmd, ["--version"], { stdio: "ignore" }).status === 0;
|
|
57
60
|
}
|
|
58
61
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
"build",
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
"
|
|
62
|
+
/** Run the full compile → wasm-bindgen → optimise pipeline. Throws via `run`'s
|
|
63
|
+
* `process.exit` on a missing tool or a non-zero step. */
|
|
64
|
+
export function buildWasm(): void {
|
|
65
|
+
console.log(
|
|
66
|
+
"→ cargo build -p rastack-wasm --release (wasm32-unknown-unknown)",
|
|
67
|
+
);
|
|
68
|
+
run(
|
|
69
|
+
"cargo",
|
|
70
|
+
[
|
|
71
|
+
"build",
|
|
72
|
+
"-p",
|
|
73
|
+
"rastack-wasm",
|
|
74
|
+
"--release",
|
|
75
|
+
"--target",
|
|
76
|
+
"wasm32-unknown-unknown",
|
|
77
|
+
],
|
|
78
|
+
rustDir,
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
console.log("→ wasm-bindgen → tools/wasm/");
|
|
82
|
+
run("wasm-bindgen", [
|
|
67
83
|
"--target",
|
|
68
|
-
"
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
84
|
+
"web",
|
|
85
|
+
"--out-dir",
|
|
86
|
+
outDir,
|
|
87
|
+
"--out-name",
|
|
88
|
+
"rastack_wasm",
|
|
89
|
+
wasmPath,
|
|
90
|
+
]);
|
|
72
91
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
"--out-name",
|
|
80
|
-
"rastack_wasm",
|
|
81
|
-
wasmPath,
|
|
82
|
-
]);
|
|
92
|
+
// Mark the emitted bundle as ESM so Node and bundlers treat it correctly
|
|
93
|
+
// without forcing the parent rastack package to be a module.
|
|
94
|
+
fs.writeFileSync(
|
|
95
|
+
path.join(outDir, "package.json"),
|
|
96
|
+
JSON.stringify({ type: "module", sideEffects: false }, null, 2) + "\n",
|
|
97
|
+
);
|
|
83
98
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
);
|
|
99
|
+
if (has("wasm-opt")) {
|
|
100
|
+
console.log("→ wasm-opt -Oz");
|
|
101
|
+
const bg = path.join(outDir, "rastack_wasm_bg.wasm");
|
|
102
|
+
run("wasm-opt", ["-Oz", "-o", bg, bg]);
|
|
103
|
+
} else {
|
|
104
|
+
console.log("· wasm-opt not found — skipping size optimisation (optional)");
|
|
105
|
+
}
|
|
90
106
|
|
|
91
|
-
|
|
92
|
-
console.log(
|
|
93
|
-
const bg = path.join(outDir, "rastack_wasm_bg.wasm");
|
|
94
|
-
run("wasm-opt", ["-Oz", "-o", bg, bg]);
|
|
95
|
-
} else {
|
|
96
|
-
console.log("· wasm-opt not found — skipping size optimisation (optional)");
|
|
107
|
+
const size = fs.statSync(path.join(outDir, "rastack_wasm_bg.wasm")).size;
|
|
108
|
+
console.log(`✓ wasm engine built: tools/wasm/ (${(size / 1024).toFixed(0)} KB)`);
|
|
97
109
|
}
|
|
98
110
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
);
|
|
111
|
+
if (require.main === module) {
|
|
112
|
+
buildWasm();
|
|
113
|
+
}
|
package/src/rastack.ts
CHANGED
|
@@ -11,9 +11,8 @@
|
|
|
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
|
|
14
|
+
* rastack dev [--port n] [--warehouse d] Local dev: run the app + admin in-browser over WASM
|
|
15
15
|
* rastack admin [--warehouse dir] Run the Rust admin (Django-admin-style DB browser)
|
|
16
|
-
* rastack wasm Compile the API to WebAssembly (in-browser local dev)
|
|
17
16
|
* rastack scan [files...] [--fail-on-pii] Scan CSVs for PII and schema info
|
|
18
17
|
* rastack seed [--env dev] [--source gs://] Load CSVs into Iceberg namespace
|
|
19
18
|
* rastack bootstrap [--owner o] [--repo r] Stand the tier-0 AWS account stack up (once per account+region)
|
|
@@ -95,16 +94,14 @@ switch (command) {
|
|
|
95
94
|
// rastack-compile dispatches on its own first argument, so pass the command.
|
|
96
95
|
run("rastack-compile.js", [command, ...rest]);
|
|
97
96
|
break;
|
|
98
|
-
case "
|
|
99
|
-
|
|
97
|
+
case "dev":
|
|
98
|
+
// Local development: serve the app + admin in the browser over the WASM
|
|
99
|
+
// engine (no native server, no cargo). Replaces `serve` + `wasm`.
|
|
100
|
+
run("rastack-dev.js", rest);
|
|
100
101
|
break;
|
|
101
102
|
case "admin":
|
|
102
103
|
runRust("admin");
|
|
103
104
|
break;
|
|
104
|
-
case "wasm":
|
|
105
|
-
// Compile the API to WebAssembly for in-browser local dev.
|
|
106
|
-
run("rastack-wasm-build.js", rest);
|
|
107
|
-
break;
|
|
108
105
|
case "tokens":
|
|
109
106
|
// Design tokens → CSS custom properties + typed theme.
|
|
110
107
|
run("rastack-tokens.js", rest);
|
|
@@ -156,8 +153,8 @@ switch (command) {
|
|
|
156
153
|
` rastack generate [--schema-only]\n` +
|
|
157
154
|
` rastack tokens [input] [outDir] [--prefix p] Compile design tokens → CSS vars + typed theme\n` +
|
|
158
155
|
` rastack design [input] [--port n] Live design-system studio (showcase + edit)\n` +
|
|
159
|
-
` rastack
|
|
160
|
-
` rastack
|
|
156
|
+
` rastack dev [--port n] [--warehouse dir] [--no-open] Run the app + admin in-browser over WASM (local dev)\n` +
|
|
157
|
+
` rastack admin [--warehouse dir] Run the native Rust admin (needs a built rastack-server)\n` +
|
|
161
158
|
` rastack scan [files...] [--fail-on-pii]\n` +
|
|
162
159
|
` rastack seed [--env dev|staging] [--source gs://bucket/ | ./myapp-dev/]\n` +
|
|
163
160
|
` rastack bootstrap [--owner o] [--repo r] [--region r] [--prefix p] [--reuse-oidc] [--print] Stand the AWS account stack up\n` +
|