rastack 0.0.64 → 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 +25 -0
- package/README.md +19 -24
- package/admin/httpEngine.ts +5 -6
- package/admin/index.tsx +11 -91
- package/dist/admin.js +14 -22
- 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 +2 -3
- 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-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.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 -157
- package/provider/types.ts +29 -77
- package/src/compile/model.ts +1 -1
- package/src/compile/run.ts +2 -2
- package/src/define/index.ts +2 -3
- 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-design.ts +1 -2
- package/src/rastack-dev.ts +79 -91
- 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
|
@@ -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,
|
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-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";
|