babavoss 0.0.1 → 0.0.2
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/NOTICE +7 -0
- package/bin/voss.ts +48 -0
- package/gui/babavoss-web.js +234 -0
- package/gui/chunk-5hpp1ypv.js +11710 -0
- package/gui/chunk-9rq662fd.js +8627 -0
- package/gui/chunk-k3cm8j6r.js +433 -0
- package/gui/chunk-pk09y98y.js +50 -0
- package/gui/chunk-smz02qa6.js +186 -0
- package/gui/chunk-wwqypxre.js +49 -0
- package/gui/gui.js +2015 -0
- package/gui/react-compiler-runtime.js +39 -0
- package/gui/react-dom-client.js +21 -0
- package/gui/react-dom.js +44 -0
- package/gui/react-jsx-runtime.js +19 -0
- package/gui/react.js +105 -0
- package/gui/theme.css +3053 -0
- package/index.ts +15 -0
- package/package.json +50 -4
- package/src/baba/check.ts +90 -0
- package/src/baba/config.ts +261 -0
- package/src/baba/find.ts +12 -0
- package/src/baba/init.ts +176 -0
- package/src/baba/node.ts +483 -0
- package/src/baba/project.ts +63 -0
- package/src/baba/registry.ts +35 -0
- package/src/baba/worker.ts +55 -0
- package/src/bench/index.ts +6 -0
- package/src/bench/measure.ts +132 -0
- package/src/bench/scenarios.ts +136 -0
- package/src/build/builder.ts +74 -0
- package/src/build/failure.ts +78 -0
- package/src/build/guard.ts +85 -0
- package/src/build/mdx-register.ts +3 -0
- package/src/build/mdx.ts +38 -0
- package/src/build/project.ts +38 -0
- package/src/build/views.ts +146 -0
- package/src/builder/main.ts +29 -0
- package/src/desktop/bob.ts +76 -0
- package/src/desktop/desktop.css +111 -0
- package/src/desktop/icons.ts +50 -0
- package/src/desktop/index.ts +323 -0
- package/src/desktop/routes.ts +98 -0
- package/src/desktop/view.tsx +673 -0
- package/src/door/core.ts +384 -0
- package/src/ecs/baba.ts +431 -0
- package/src/ecs/codec.ts +334 -0
- package/src/ecs/handles.ts +91 -0
- package/src/ecs/replica.ts +150 -0
- package/src/ecs/runtime.ts +603 -0
- package/src/ecs/scheduler.ts +75 -0
- package/src/ecs/snapshot.ts +102 -0
- package/src/ecs/state.ts +759 -0
- package/src/ecs/system.ts +256 -0
- package/src/ecs/table.ts +420 -0
- package/src/ecs/testbed.ts +97 -0
- package/src/exec/host.ts +177 -0
- package/src/exec/main.ts +98 -0
- package/src/exec/watch.ts +7 -0
- package/src/exec/wire.ts +29 -0
- package/src/generated/build.ts +4 -0
- package/src/gui/css.d.ts +1 -0
- package/src/gui/gui.tsx +245 -0
- package/src/gui/index.ts +51 -0
- package/src/gui/inspector.tsx +47 -0
- package/src/gui/levels.tsx +73 -0
- package/src/gui/promptware.tsx +68 -0
- package/src/gui/runner.tsx +118 -0
- package/src/gui/theme.css +498 -0
- package/src/gui/theme.ts +25 -0
- package/src/gui/wizard.tsx +227 -0
- package/src/guide/add-a-desktop.mdx +100 -0
- package/src/guide/compose-an-interface.mdx +84 -0
- package/src/guide/index.ts +13 -0
- package/src/guide/reach-outside.mdx +112 -0
- package/src/guide/spec-a-system.mdx +93 -0
- package/src/guide/systems-together.mdx +72 -0
- package/src/guide/write-a-system.mdx +183 -0
- package/src/guide/write-promptware.mdx +90 -0
- package/src/http/server.ts +310 -0
- package/src/kernel/build.ts +21 -0
- package/src/kernel/builder.ts +105 -0
- package/src/kernel/children.ts +117 -0
- package/src/kernel/context.ts +90 -0
- package/src/kernel/lock.ts +46 -0
- package/src/kernel/names.ts +14 -0
- package/src/kernel/schema.ts +130 -0
- package/src/kernel/where.ts +12 -0
- package/src/kit/index.ts +232 -0
- package/src/maker/system.ts +213 -0
- package/src/mcp/daemon.ts +61 -0
- package/src/mcp/main.ts +208 -0
- package/src/mcp/rpc.ts +64 -0
- package/src/mcp/tools.ts +125 -0
- package/src/prompt/evals.ts +42 -0
- package/src/prompt/index.ts +242 -0
- package/src/prompt/jsx-dev-runtime.ts +1 -0
- package/src/prompt/jsx-runtime.ts +49 -0
- package/src/prompt/mdx.d.ts +1 -0
- package/src/promptware/compile.ts +183 -0
- package/src/promptware/define.ts +16 -0
- package/src/promptware/disk.ts +72 -0
- package/src/promptware/markdown.d.ts +6 -0
- package/src/promptware/sync.ts +437 -0
- package/src/promptware/system.ts +215 -0
- package/src/runtime/bridge.ts +85 -0
- package/src/runtime/connect.ts +54 -0
- package/src/runtime/env.ts +35 -0
- package/src/runtime/harness.ts +80 -0
- package/src/runtime/main.ts +119 -0
- package/src/runtime/worker.ts +33 -0
- package/src/server/edge.ts +332 -0
- package/src/server/main.ts +45 -0
- package/src/server/messages.ts +97 -0
- package/src/server/protocol.ts +37 -0
- package/src/services/args.ts +45 -0
- package/src/services/exec.ts +69 -0
- package/src/services/fs.ts +139 -0
- package/src/services/http.ts +30 -0
- package/src/services/index.ts +113 -0
- package/src/services/secrets.ts +18 -0
- package/src/shell/address.ts +21 -0
- package/src/shell/args.ts +219 -0
- package/src/shell/client.ts +107 -0
- package/src/shell/codes.ts +26 -0
- package/src/shell/positional.ts +20 -0
- package/src/shell/run.ts +470 -0
- package/src/shell/service.ts +167 -0
- package/src/shell/state.ts +204 -0
- package/src/spec/adapters.ts +72 -0
- package/src/spec/diff.ts +26 -0
- package/src/spec/files.ts +17 -0
- package/src/spec/index.ts +155 -0
- package/src/spec/run.ts +97 -0
- package/src/spec/take.ts +54 -0
- package/src/test/index.ts +8 -0
- package/src/test/prove.ts +56 -0
- package/src/test/records.ts +23 -0
- package/src/test/specs.ts +56 -0
- package/src/test/steps.ts +100 -0
- package/src/test/voss-dir.ts +17 -0
- package/src/transport/messages.ts +110 -0
- package/src/transport/transport.ts +62 -0
- package/src/wall/probe.ts +67 -0
- package/src/wall/profile.ts +103 -0
- package/src/wall/spawn.ts +59 -0
- package/src/web/app.tsx +53 -0
- package/src/web/core.tsx +140 -0
- package/src/web/form.ts +155 -0
- package/src/web/hooks.ts +135 -0
- package/src/web/index.tsx +17 -0
- package/src/web/list.ts +19 -0
- package/src/web/maker.tsx +766 -0
- package/src/web/objects.tsx +213 -0
- package/src/web/socket.ts +84 -0
- package/src/web/state.tsx +69 -0
- package/src/web/store.ts +221 -0
- package/src/web/ui.tsx +135 -0
- package/README.md +0 -5
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// One controlled measurement: a scenario at a size, run for a number of
|
|
2
|
+
// rounds with the knobs set, timed round by round, and followed the way a
|
|
3
|
+
// view follows: each round's answer made by the runtime, shipped as a frame
|
|
4
|
+
// of bytes (or as JSON, by a knob), applied to a replica and read as the
|
|
5
|
+
// view would read it. The same seed
|
|
6
|
+
// gives the same state, so two measurements differ only in what the lab
|
|
7
|
+
// changed: the knobs, or the framework underneath.
|
|
8
|
+
import { Runtime } from "../ecs/runtime.ts";
|
|
9
|
+
import { Replica } from "../ecs/replica.ts";
|
|
10
|
+
import { decodeFrame, encodeFrame } from "../ecs/codec.ts";
|
|
11
|
+
import type { StateAnswer } from "../ecs/snapshot.ts";
|
|
12
|
+
import { scenarios, rng } from "./scenarios.ts";
|
|
13
|
+
|
|
14
|
+
/** What a measurement may vary without changing the code. */
|
|
15
|
+
export interface Knobs {
|
|
16
|
+
/** Every write checked against its schema, as a testbed does; off is how the runtime runs. */
|
|
17
|
+
checking?: boolean;
|
|
18
|
+
/** Ship each follower the whole state after each round instead of its delta: what every round cost before deltas. */
|
|
19
|
+
whole?: boolean;
|
|
20
|
+
/** How many followers take each round's answer, each with its own replica; 1 by default. The follower costs are summed over them. */
|
|
21
|
+
followers?: number;
|
|
22
|
+
/** Ship each round as JSON text, as the long poll did, instead of as a frame of bytes. */
|
|
23
|
+
json?: boolean;
|
|
24
|
+
/** The audit of what changed, at each round's end; on by default, as the runtime runs. */
|
|
25
|
+
audit?: boolean;
|
|
26
|
+
/** The round's virtual time step, ms; 200 by default. */
|
|
27
|
+
tick?: number;
|
|
28
|
+
seed?: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* One round: what it cost the runtime (`ms`), and what following it cost:
|
|
33
|
+
* the answer made and serialized (`stateMs`, `bytes`), parsed and applied
|
|
34
|
+
* to the replica (`applyMs`), read by the view's query (`readMs`).
|
|
35
|
+
*/
|
|
36
|
+
export interface Sample { round: number; ms: number; bytes: number; heap: number; stateMs: number; applyMs: number; readMs: number }
|
|
37
|
+
|
|
38
|
+
export interface Summary {
|
|
39
|
+
rounds: number; entities: number;
|
|
40
|
+
avgMs: number; p50Ms: number; p95Ms: number; maxMs: number; totalMs: number;
|
|
41
|
+
avgBytes: number; totalBytes: number;
|
|
42
|
+
/** Following, per round: the answer made, applied, read; and their sum. */
|
|
43
|
+
avgStateMs: number; avgApplyMs: number; avgReadMs: number; avgFollowMs: number; p95FollowMs: number;
|
|
44
|
+
/** Heap in use after a collection before the rounds, after them, and the highest seen between. */
|
|
45
|
+
heapStart: number; heapEnd: number; heapPeak: number;
|
|
46
|
+
/** How much the rounds left behind: heapEnd - heapStart. */
|
|
47
|
+
retained: number;
|
|
48
|
+
seedMs: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface Measurement { scenario: string; size: number; knobs: Knobs; summary: Summary; series: Sample[]; at: number; runtime: string }
|
|
52
|
+
|
|
53
|
+
// The host, when there is one: a baba bundle may not name process or Bun, and a browser has neither.
|
|
54
|
+
const host = globalThis as unknown as { process?: { memoryUsage?: () => { heapUsed: number } }; Bun?: { gc?: (sync: boolean) => void; version?: string } };
|
|
55
|
+
const heapNow = () => host.process?.memoryUsage?.().heapUsed ?? 0;
|
|
56
|
+
const gc = () => { host.Bun?.gc?.(true); };
|
|
57
|
+
const quantile = (xs: number[], q: number) => { const a = [...xs].sort((x, y) => x - y); return a.length ? a[Math.min(a.length - 1, Math.floor(q * a.length))]! : 0; };
|
|
58
|
+
const mean = (xs: number[]) => xs.reduce((a, b) => a + b, 0) / Math.max(1, xs.length);
|
|
59
|
+
|
|
60
|
+
/** Runs the scenario and answers what it cost; `points` bounds the series kept. */
|
|
61
|
+
export function measure(scenario: string, size: number, rounds: number, knobs: Knobs = {}, points = 200): Measurement {
|
|
62
|
+
const sc = scenarios[scenario];
|
|
63
|
+
if (!sc) throw new Error(`no scenario ${scenario}; scenarios: ${Object.keys(scenarios).join(", ")}`);
|
|
64
|
+
const tick = knobs.tick ?? 200, followers = Math.max(1, knobs.followers ?? 1);
|
|
65
|
+
let now = 1_000;
|
|
66
|
+
const rt = new Runtime(sc.baba, { auto: false, now: () => now, audit: knobs.audit !== false });
|
|
67
|
+
rt.w.checking = knobs.checking === true;
|
|
68
|
+
const seeded = performance.now();
|
|
69
|
+
sc.seed(rt, size, rng(knobs.seed ?? 1));
|
|
70
|
+
const seedMs = performance.now() - seeded;
|
|
71
|
+
rt.step(now); // the first round seeds the scenario's own random and settles the arrivals
|
|
72
|
+
// The followers: each holds a replica and takes every round's answer, as a view does: the whole on first contact, then deltas.
|
|
73
|
+
const views = Array.from({ length: followers }, () => ({ since: 0, replica: new Replica() }));
|
|
74
|
+
const follow = () => {
|
|
75
|
+
let bytes = 0, stateMs = 0, applyMs = 0, readMs = 0;
|
|
76
|
+
for (const v of views) {
|
|
77
|
+
const t0 = performance.now();
|
|
78
|
+
let shipped: number, t1: number, parsed: StateAnswer;
|
|
79
|
+
if (knobs.json) {
|
|
80
|
+
const answer: StateAnswer = knobs.whole ? rt.snapshot({ redacted: true }) : rt.state(v.since, { redacted: true });
|
|
81
|
+
const text = JSON.stringify(answer);
|
|
82
|
+
shipped = text.length; t1 = performance.now();
|
|
83
|
+
parsed = JSON.parse(text) as StateAnswer;
|
|
84
|
+
} else {
|
|
85
|
+
const frame = knobs.whole ? encodeFrame(rt.codec, rt.snapshot({ redacted: true })) : rt.frame(v.since);
|
|
86
|
+
shipped = frame.byteLength; t1 = performance.now();
|
|
87
|
+
parsed = decodeFrame(rt.codec, frame);
|
|
88
|
+
}
|
|
89
|
+
v.replica = v.replica.apply(parsed);
|
|
90
|
+
v.since = rt.round;
|
|
91
|
+
const t2 = performance.now();
|
|
92
|
+
sc.view(v.replica);
|
|
93
|
+
const t3 = performance.now();
|
|
94
|
+
bytes += shipped; stateMs += t1 - t0; applyMs += t2 - t1; readMs += t3 - t2;
|
|
95
|
+
}
|
|
96
|
+
return { bytes, stateMs, applyMs, readMs };
|
|
97
|
+
};
|
|
98
|
+
follow();
|
|
99
|
+
gc();
|
|
100
|
+
const heapStart = heapNow();
|
|
101
|
+
let heapPeak = heapStart;
|
|
102
|
+
const series: Sample[] = [];
|
|
103
|
+
const ms: number[] = [], stateMs: number[] = [], applyMs: number[] = [], readMs: number[] = [];
|
|
104
|
+
let totalBytes = 0;
|
|
105
|
+
const every = Math.max(1, Math.floor(rounds / points));
|
|
106
|
+
for (let r = 1; r <= rounds; r++) {
|
|
107
|
+
now += tick;
|
|
108
|
+
const t0 = performance.now();
|
|
109
|
+
rt.step(now);
|
|
110
|
+
const dt = performance.now() - t0;
|
|
111
|
+
const f = follow();
|
|
112
|
+
ms.push(dt); stateMs.push(f.stateMs); applyMs.push(f.applyMs); readMs.push(f.readMs);
|
|
113
|
+
totalBytes += f.bytes;
|
|
114
|
+
if (r % every === 0 || r === rounds) {
|
|
115
|
+
const heap = heapNow();
|
|
116
|
+
if (heap > heapPeak) heapPeak = heap;
|
|
117
|
+
series.push({ round: rt.round, ms: dt, bytes: f.bytes, heap, stateMs: f.stateMs, applyMs: f.applyMs, readMs: f.readMs });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
gc();
|
|
121
|
+
const heapEnd = heapNow();
|
|
122
|
+
const followMs = ms.map((_, i) => stateMs[i]! + applyMs[i]! + readMs[i]!);
|
|
123
|
+
const summary: Summary = {
|
|
124
|
+
rounds, entities: rt.w.all().length,
|
|
125
|
+
avgMs: mean(ms), p50Ms: quantile(ms, 0.5), p95Ms: quantile(ms, 0.95), maxMs: Math.max(0, ...ms), totalMs: ms.reduce((a, b) => a + b, 0),
|
|
126
|
+
avgBytes: totalBytes / Math.max(1, rounds), totalBytes,
|
|
127
|
+
avgStateMs: mean(stateMs), avgApplyMs: mean(applyMs), avgReadMs: mean(readMs), avgFollowMs: mean(followMs), p95FollowMs: quantile(followMs, 0.95),
|
|
128
|
+
heapStart, heapEnd, heapPeak: Math.max(heapPeak, heapEnd), retained: heapEnd - heapStart, seedMs,
|
|
129
|
+
};
|
|
130
|
+
rt.stop();
|
|
131
|
+
return { scenario, size, knobs, summary, series, at: Date.now(), runtime: host.Bun?.version ? `bun ${host.Bun.version}` : "js" };
|
|
132
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// The workloads the lab measures: each a baba and a way to seed it to a
|
|
2
|
+
// size, deterministic from a seed so two runs differ only in what the lab
|
|
3
|
+
// changed. Pure ECS: no port, no view.
|
|
4
|
+
import { baba, resource } from "../ecs/baba.ts";
|
|
5
|
+
import { system } from "../ecs/system.ts";
|
|
6
|
+
import { s } from "../kernel/schema.ts";
|
|
7
|
+
import { not, added, removed, changed, select } from "../ecs/state.ts";
|
|
8
|
+
import type { Baba } from "../ecs/baba.ts";
|
|
9
|
+
import type { Runtime } from "../ecs/runtime.ts";
|
|
10
|
+
import type { Replica } from "../ecs/replica.ts";
|
|
11
|
+
export { not, added, removed, changed, select };
|
|
12
|
+
|
|
13
|
+
/** A deterministic random: the same seed, the same state. */
|
|
14
|
+
export function rng(seed: number): () => number {
|
|
15
|
+
let x = seed >>> 0 || 1;
|
|
16
|
+
return () => { x ^= x << 13; x >>>= 0; x ^= x >>> 17; x ^= x << 5; x >>>= 0; return x / 4294967296; };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface Scenario {
|
|
20
|
+
name: string;
|
|
21
|
+
summary: string;
|
|
22
|
+
baba: Baba;
|
|
23
|
+
/** Fills the state to `size` entities, with `random` for the positions and the like. */
|
|
24
|
+
seed(rt: Runtime, size: number, random: () => number): void;
|
|
25
|
+
/** What a view of it reads each round: the rows it would draw. */
|
|
26
|
+
view(r: Replica): void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const W = 600, H = 400;
|
|
30
|
+
const dist = (a: { x: number; y: number }, b: { x: number; y: number }) => Math.hypot(a.x - b.x, a.y - b.y);
|
|
31
|
+
|
|
32
|
+
/** The pond: every fish moves every round; a tenth as much food; fish seek, eat, food rots. The demo's system, made deterministic. */
|
|
33
|
+
let pondRandom = rng(1);
|
|
34
|
+
const pond = system({
|
|
35
|
+
name: "pond",
|
|
36
|
+
model: {
|
|
37
|
+
fish: { x: s.number(), y: s.number(), vx: s.number(), vy: s.number(), hunger: s.number() },
|
|
38
|
+
food: { x: s.number(), y: s.number(), at: s.integer() },
|
|
39
|
+
target: { food: s.entity("food") },
|
|
40
|
+
fed: resource(s.integer(), 0), seed: resource(s.integer(), 1),
|
|
41
|
+
},
|
|
42
|
+
}, (pond, p) => {
|
|
43
|
+
const idle = select(pond.fish, not(pond.target));
|
|
44
|
+
const chasing = select(pond.fish, pond.target);
|
|
45
|
+
return [
|
|
46
|
+
p.step("seek", (w) => {
|
|
47
|
+
let freed = false;
|
|
48
|
+
w.each([removed(pond.food)], (gone) => { w.each(chasing, (e, _, t) => { if (t.food === gone) { w.remove(e, pond.target); freed = true; } }); });
|
|
49
|
+
if (!freed && w.query(added(pond.food)).length === 0 && w.query(removed(pond.target)).length === 0) return;
|
|
50
|
+
const foods = w.query(pond.food);
|
|
51
|
+
if (foods.length === 0) return;
|
|
52
|
+
w.each(idle, (e, f) => {
|
|
53
|
+
let best = foods[0]!, bestD = Infinity;
|
|
54
|
+
for (const c of foods) { const d = dist(f, c.food); if (d < bestD) { best = c; bestD = d; } }
|
|
55
|
+
w.add(e, pond.target, { food: best.entity });
|
|
56
|
+
});
|
|
57
|
+
}),
|
|
58
|
+
|
|
59
|
+
p.step("swim", (w) => {
|
|
60
|
+
if (w.round === 1) pondRandom = rng(w.get(pond.seed)); // before the first round's early return: the sequence starts over for every measurement
|
|
61
|
+
const dt = Math.min(w.elapsed / 1000, 0.5);
|
|
62
|
+
if (dt <= 0) return;
|
|
63
|
+
w.each([pond.fish], (e, r) => {
|
|
64
|
+
let { x, y, vx, vy, hunger } = r;
|
|
65
|
+
const t = w.get(e, pond.target);
|
|
66
|
+
const goal = t && w.get(t.food, pond.food);
|
|
67
|
+
if (goal) { const d = dist(r, goal) || 1; vx = (goal.x - x) / d * 140; vy = (goal.y - y) / d * 140; }
|
|
68
|
+
else { const a = Math.atan2(vy, vx) + (pondRandom() - 0.5) * 1.2; vx = Math.cos(a) * 60; vy = Math.sin(a) * 60; }
|
|
69
|
+
x += vx * dt; y += vy * dt;
|
|
70
|
+
if (x < 0) { x = 0; vx = Math.abs(vx); } else if (x > W) { x = W; vx = -Math.abs(vx); }
|
|
71
|
+
if (y < 0) { y = 0; vy = Math.abs(vy); } else if (y > H) { y = H; vy = -Math.abs(vy); }
|
|
72
|
+
w.add(e, pond.fish, { x, y, vx, vy, hunger: hunger + dt });
|
|
73
|
+
});
|
|
74
|
+
}),
|
|
75
|
+
|
|
76
|
+
p.step("eat", (w) => {
|
|
77
|
+
w.each([changed(pond.fish), pond.target], (e, r, t) => {
|
|
78
|
+
const f = w.get(t.food, pond.food);
|
|
79
|
+
if (!f || dist(r, f) > 12) return;
|
|
80
|
+
w.despawn(t.food);
|
|
81
|
+
w.remove(e, pond.target);
|
|
82
|
+
w.update(e, pond.fish, (x) => ({ ...x, hunger: 0 }));
|
|
83
|
+
w.update(pond.fed, (n) => n + 1);
|
|
84
|
+
});
|
|
85
|
+
}),
|
|
86
|
+
|
|
87
|
+
p.step("rot", (w) => { w.each([pond.food], (e, f) => { if (w.now - f.at > 30_000) w.despawn(e); }); }),
|
|
88
|
+
];
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
/** Churn: a tenth of the items change a value each round, a twentieth gain or lose a tag: what a reconciling system does. */
|
|
92
|
+
let churnRandom = rng(1);
|
|
93
|
+
const churn = system({
|
|
94
|
+
name: "churn",
|
|
95
|
+
model: { item: { n: s.integer(), text: s.string() }, hot: s.tag(), seed: resource(s.integer(), 1) },
|
|
96
|
+
}, (churn, p) => [
|
|
97
|
+
p.step("move", (w) => {
|
|
98
|
+
if (w.round === 1) churnRandom = rng(w.get(churn.seed));
|
|
99
|
+
w.each([churn.item], (e, v) => {
|
|
100
|
+
const r = churnRandom();
|
|
101
|
+
if (r < 0.1) w.add(e, churn.item, { n: v.n + 1, text: v.text });
|
|
102
|
+
else if (r < 0.15) { if (w.has(e, churn.hot)) w.remove(e, churn.hot); else w.add(e, churn.hot); }
|
|
103
|
+
});
|
|
104
|
+
}),
|
|
105
|
+
p.step("count", (w) => { let n = 0; w.each([churn.hot], () => { n++; }); void n; }),
|
|
106
|
+
]);
|
|
107
|
+
|
|
108
|
+
/** Quiet: items that never change; a step that reads them all: the floor of a round, and what a delta costs when nothing moved. */
|
|
109
|
+
const quiet = system({ name: "quiet", model: { item: { n: s.integer() } } }, (quiet, p) => [
|
|
110
|
+
p.step("read", (w) => { let n = 0; w.each([quiet.item], (_, v) => { n += v.n; }); void n; }),
|
|
111
|
+
]);
|
|
112
|
+
|
|
113
|
+
export const scenarios: Record<string, Scenario> = {
|
|
114
|
+
pond: {
|
|
115
|
+
name: "pond", summary: "fish that all move every round, food a tenth as many; seek, eat, rot",
|
|
116
|
+
baba: baba({ systems: [pond] }),
|
|
117
|
+
seed(rt, size, random) {
|
|
118
|
+
const w = rt.w;
|
|
119
|
+
for (let i = 0; i < size; i++) { const a = random() * Math.PI * 2; w.spawn(pond.fish, { x: random() * W, y: random() * H, vx: Math.cos(a) * 60, vy: Math.sin(a) * 60, hunger: 0 }); }
|
|
120
|
+
for (let i = 0; i < Math.max(1, size / 10); i++) w.spawn(pond.food, { x: random() * W, y: random() * H, at: 0 });
|
|
121
|
+
},
|
|
122
|
+
view(r) { r.query(pond.fish); r.query(pond.food); },
|
|
123
|
+
},
|
|
124
|
+
churn: {
|
|
125
|
+
name: "churn", summary: "a tenth of the items rewritten and a twentieth tagged or untagged each round",
|
|
126
|
+
baba: baba({ systems: [churn] }),
|
|
127
|
+
seed(rt, size, random) { for (let i = 0; i < size; i++) rt.w.spawn(churn.item, { n: Math.floor(random() * 100), text: `item ${i}` }); },
|
|
128
|
+
view(r) { r.query(churn.item); r.query(churn.hot); },
|
|
129
|
+
},
|
|
130
|
+
quiet: {
|
|
131
|
+
name: "quiet", summary: "items that never change, read by one step: the floor",
|
|
132
|
+
baba: baba({ systems: [quiet] }),
|
|
133
|
+
seed(rt, size, random) { for (let i = 0; i < size; i++) rt.w.spawn(quiet.item, { n: Math.floor(random() * 100) }); },
|
|
134
|
+
view(r) { r.query(quiet.item); },
|
|
135
|
+
},
|
|
136
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// What a build is, wherever it runs: the Builder a Node asks, the outputs
|
|
2
|
+
// a build answers with, and the checks the kernel makes on them before
|
|
3
|
+
// trusting a path. The in-process builder here is for tests and the worker
|
|
4
|
+
// transport; the kernel's own runs a walled process (src/kernel/builder.ts).
|
|
5
|
+
import { posix } from "node:path";
|
|
6
|
+
import type { Project } from "../baba/project.ts";
|
|
7
|
+
import { bundleProject } from "./project.ts";
|
|
8
|
+
import { bundleView } from "./views.ts";
|
|
9
|
+
|
|
10
|
+
/** One file a build produced, as bytes: its path under the view's URL, and its kind. */
|
|
11
|
+
export interface BuildOutput {
|
|
12
|
+
path: string;
|
|
13
|
+
kind: "entry-point" | "chunk" | "asset" | "sourcemap" | "bytecode";
|
|
14
|
+
/** The media type to serve it with. */
|
|
15
|
+
type: string;
|
|
16
|
+
bytes: Uint8Array;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** The view to build: where it is, where its outputs will be served, and what the project declared for the build. */
|
|
20
|
+
export interface ViewSpec {
|
|
21
|
+
entry: string;
|
|
22
|
+
/** The project's .baba: paths in failures are relative to it; build plugins resolve from it; asset directories are under it. */
|
|
23
|
+
root: string;
|
|
24
|
+
publicPath: string;
|
|
25
|
+
dir: string;
|
|
26
|
+
plugins: string[];
|
|
27
|
+
assets: string[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface Builder {
|
|
31
|
+
/** The baba's backend bundle: one module that serves itself in a Worker. */
|
|
32
|
+
project(p: Project): Promise<string>;
|
|
33
|
+
/** A view's bundle for the browser, every output; URLs inside it start with `publicPath`. */
|
|
34
|
+
view(spec: ViewSpec): Promise<BuildOutput[]>;
|
|
35
|
+
stop(): void;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export class InProcessBuilder implements Builder {
|
|
39
|
+
project(p: Project) { return bundleProject(p); }
|
|
40
|
+
async view(spec: ViewSpec) { return validateOutputs(await bundleView(spec)); }
|
|
41
|
+
stop() {}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const limits = { count: 512, bytes: 256 * 1024 * 1024 };
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Outputs come from a process that ran the project's code: a path is
|
|
48
|
+
* trusted only when it is a plain relative one, once, and the whole fits.
|
|
49
|
+
*/
|
|
50
|
+
export function validateOutputs(outputs: BuildOutput[]): BuildOutput[] {
|
|
51
|
+
if (!Array.isArray(outputs) || outputs.length === 0) throw new Error("the build produced no outputs");
|
|
52
|
+
if (outputs.length > limits.count) throw new Error(`the build produced ${outputs.length} outputs; at most ${limits.count} are served`);
|
|
53
|
+
const seen = new Set<string>();
|
|
54
|
+
let total = 0;
|
|
55
|
+
let entries = 0;
|
|
56
|
+
for (const o of outputs) {
|
|
57
|
+
if (typeof o.path !== "string" || o.path === "" || o.path.startsWith("/") || posix.normalize(o.path) !== o.path || o.path.split("/").some((s) => s === "" || s === "." || s === "..")) {
|
|
58
|
+
throw new Error(`the build named an output ${JSON.stringify(o.path)}; outputs are plain relative paths`);
|
|
59
|
+
}
|
|
60
|
+
if (seen.has(o.path)) throw new Error(`the build named ${o.path} twice`);
|
|
61
|
+
seen.add(o.path);
|
|
62
|
+
if (!(o.bytes instanceof Uint8Array)) throw new Error(`the output ${o.path} is not bytes`);
|
|
63
|
+
total += o.bytes.byteLength;
|
|
64
|
+
if (o.kind === "entry-point") entries++;
|
|
65
|
+
}
|
|
66
|
+
if (total > limits.bytes) throw new Error(`the build produced ${(total / 1048576).toFixed(0)} MB; at most ${limits.bytes / 1048576} MB are served`);
|
|
67
|
+
if (entries !== 1) throw new Error(`the build produced ${entries} entry points; a view has one`);
|
|
68
|
+
return outputs;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** The entry point's text. */
|
|
72
|
+
export function entryOf(outputs: BuildOutput[]): string {
|
|
73
|
+
return new TextDecoder().decode(outputs.find((o) => o.kind === "entry-point")!.bytes);
|
|
74
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// What went wrong with a view, in one shape from the bundler to the window:
|
|
2
|
+
// where it failed, one line saying what, the place in the source when
|
|
3
|
+
// there is one, and the rest for whoever wants it. Pure, so the GUI
|
|
4
|
+
// and the kernel share it.
|
|
5
|
+
|
|
6
|
+
export type Stage = "build" | "load" | "render";
|
|
7
|
+
|
|
8
|
+
export interface ViewFailure {
|
|
9
|
+
stage: Stage;
|
|
10
|
+
/** One line, for a person. */
|
|
11
|
+
message: string;
|
|
12
|
+
/** The source file, relative to the project's `.baba` when it is inside. */
|
|
13
|
+
file?: string;
|
|
14
|
+
line?: number;
|
|
15
|
+
column?: number;
|
|
16
|
+
/** The source line, when the bundler had it. */
|
|
17
|
+
excerpt?: string;
|
|
18
|
+
/** Further messages, a stack, a component stack. */
|
|
19
|
+
detail?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** The bundler's message as Bun.build logs it. */
|
|
23
|
+
interface Logged {
|
|
24
|
+
level?: string;
|
|
25
|
+
message: string;
|
|
26
|
+
position?: { file?: string; line?: number; column?: number; lineText?: string } | null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** A path under `root` as `root` sees it; anything else as it is. */
|
|
30
|
+
export function within(root: string, file: string): string {
|
|
31
|
+
const base = root.endsWith("/") ? root : root + "/";
|
|
32
|
+
return file.startsWith(base) ? file.slice(base.length) : file;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* A failed build as one failure: the first error is the headline, the
|
|
37
|
+
* others go to the detail, and paths inside the project lose their prefix.
|
|
38
|
+
*/
|
|
39
|
+
export function buildFailure(logs: readonly Logged[], root: string): ViewFailure {
|
|
40
|
+
const errors = logs.filter((l) => l.level === undefined || l.level === "error");
|
|
41
|
+
const [first, ...rest] = errors.length > 0 ? errors : logs;
|
|
42
|
+
const shorten = (s: string) => s.split(root.endsWith("/") ? root : root + "/").join("");
|
|
43
|
+
if (!first) return { stage: "build", message: "the bundler failed without a message" };
|
|
44
|
+
const f: ViewFailure = { stage: "build", message: shorten(first.message.replace(/^BuildMessage: /, "")) };
|
|
45
|
+
const p = first.position;
|
|
46
|
+
if (p?.file) {
|
|
47
|
+
f.file = within(root, p.file);
|
|
48
|
+
if (p.line) { f.line = p.line; if (p.column) f.column = p.column; }
|
|
49
|
+
if (p.lineText) f.excerpt = p.lineText;
|
|
50
|
+
}
|
|
51
|
+
if (rest.length > 0) f.detail = rest.map((l) => shorten(l.message) + (l.position?.file ? ` (${within(root, l.position.file)}${l.position.line ? `:${l.position.line}` : ""})` : "")).join("\n");
|
|
52
|
+
return f;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Where a failure points: `file:line:column`, as much as is known. */
|
|
56
|
+
export function placeOf(f: ViewFailure): string | undefined {
|
|
57
|
+
if (!f.file) return undefined;
|
|
58
|
+
return f.file + (f.line ? `:${f.line}` + (f.column ? `:${f.column}` : "") : "");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The failure as text: for a log line, the shell, or a clipboard. */
|
|
62
|
+
export function formatFailure(f: ViewFailure): string {
|
|
63
|
+
const head = { build: "The view did not build", load: "The view did not load", render: "The view crashed while rendering" }[f.stage];
|
|
64
|
+
const lines = [`${head}: ${f.message}`];
|
|
65
|
+
const place = placeOf(f);
|
|
66
|
+
if (place) lines.push(` at ${place}`);
|
|
67
|
+
if (f.excerpt) {
|
|
68
|
+
lines.push(` ${f.excerpt}`);
|
|
69
|
+
if (f.column) lines.push(` ${" ".repeat(f.column - 1)}^`);
|
|
70
|
+
}
|
|
71
|
+
if (f.detail) lines.push("", f.detail);
|
|
72
|
+
return lines.join("\n");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** A thrown failure, so a caller can tell one from any other error. */
|
|
76
|
+
export class ViewFailed extends Error {
|
|
77
|
+
constructor(readonly failure: ViewFailure) { super(formatFailure(failure)); }
|
|
78
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// The guard: what a baba may not reach for, and what it uses instead.
|
|
2
|
+
// One list serves the generated declarations (.voss/guard.d.ts, where a
|
|
3
|
+
// forbidden name has a type that says the alternative), the checker's
|
|
4
|
+
// hints, and the bundler plugin that refuses the imports. It is the
|
|
5
|
+
// experience, not the guarantee: the wall underneath holds either way.
|
|
6
|
+
import { relative } from "node:path";
|
|
7
|
+
|
|
8
|
+
/** A refusal: what to use instead, and the type name that says so in the checker's errors. */
|
|
9
|
+
export interface Refusal {
|
|
10
|
+
/** What the baba uses instead, as written in the message. */
|
|
11
|
+
use: string;
|
|
12
|
+
/** The type the declaration carries, so the error names the alternative. */
|
|
13
|
+
type: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const context = "an effect's context (ctx.fs, ctx.http, ctx.exec, ctx.secrets)";
|
|
17
|
+
const fs: Refusal = { use: "ctx.fs", type: "UseCtxFsInstead" };
|
|
18
|
+
const exec: Refusal = { use: "ctx.exec", type: "UseCtxExecInstead" };
|
|
19
|
+
const http: Refusal = { use: "ctx.http", type: "UseCtxHttpInstead" };
|
|
20
|
+
const whole: Refusal = { use: context, type: "UseTheContextInstead" };
|
|
21
|
+
|
|
22
|
+
/** Module specifiers a baba may not import, each with its alternative. */
|
|
23
|
+
export const forbiddenModules: Record<string, Refusal> = {
|
|
24
|
+
"node:fs": fs, "fs": fs, "node:fs/promises": fs, "fs/promises": fs,
|
|
25
|
+
"node:child_process": exec, "child_process": exec, "bun:ffi": exec,
|
|
26
|
+
"bun:sqlite": whole,
|
|
27
|
+
"node:net": http, "net": http, "node:http": http, "http": http, "node:https": http, "https": http,
|
|
28
|
+
"node:os": whole, "os": whole, "node:process": whole, "process": whole,
|
|
29
|
+
"node:worker_threads": whole, "worker_threads": whole, "bun": whole, "bun:jsc": whole,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/** Global names a baba's backend does not have, each with its alternative. */
|
|
33
|
+
export const forbiddenGlobals: Record<string, Refusal> = {
|
|
34
|
+
Bun: { use: context, type: "UseTheContextInsteadOfBun" },
|
|
35
|
+
fetch: { use: "ctx.http.fetch", type: "UseCtxHttpFetchInstead" },
|
|
36
|
+
process: { use: "ctx.env for the environment, ctx.exec for programs", type: "UseCtxEnvOrCtxExecInsteadOfProcess" },
|
|
37
|
+
require: { use: "import", type: "UseImportInsteadOfRequire" },
|
|
38
|
+
Worker: { use: "an effect, which the runtime runs outside the round", type: "UseAnEffectInsteadOfWorker" },
|
|
39
|
+
WebSocket: { use: "ctx.http", type: "UseCtxHttpInsteadOfWebSocket" },
|
|
40
|
+
Buffer: { use: "Uint8Array, TextEncoder and TextDecoder", type: "UseUint8ArrayInsteadOfBuffer" },
|
|
41
|
+
__dirname: { use: "ctx.project", type: "UseCtxProjectInsteadOfDirname" },
|
|
42
|
+
__filename: { use: "ctx.project", type: "UseCtxProjectInsteadOfFilename" },
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/** The sentence every refusal ends with. */
|
|
46
|
+
export function advice(r: Refusal): string {
|
|
47
|
+
return `a baba has no OS: use ${r.use} instead (see README.md)`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const escape = (s: string) => s.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&");
|
|
51
|
+
const filter = new RegExp(`^(${Object.keys(forbiddenModules).map(escape).join("|")})$`);
|
|
52
|
+
|
|
53
|
+
const where = (file: string) => {
|
|
54
|
+
const rel = relative(process.cwd(), file);
|
|
55
|
+
return rel && !rel.startsWith("..") ? rel : file;
|
|
56
|
+
};
|
|
57
|
+
/** An import attribute asking for a macro, in either spelling. */
|
|
58
|
+
const macro = /\b(?:with|assert)\s*\{\s*type\s*:\s*["']macro["']\s*\}/;
|
|
59
|
+
const refuse = (file: string, specifier: string) => new Error(`${where(file)} imports ${specifier}; ${advice(forbiddenModules[specifier]!)}`);
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The bundler's refusal: a Bun.build plugin that fails the build on a
|
|
63
|
+
* forbidden import with `<file> imports <specifier>; a baba has no OS: ...`.
|
|
64
|
+
* For the browser and node targets resolution reaches the import; for the
|
|
65
|
+
* bun target a builtin never does, so each source is also scanned on load.
|
|
66
|
+
*/
|
|
67
|
+
export const guardImports: import("bun").BunPlugin = {
|
|
68
|
+
name: "babavoss-guard",
|
|
69
|
+
setup(b) {
|
|
70
|
+
b.onResolve({ filter }, (a) => { throw refuse(a.importer || a.path, a.path); });
|
|
71
|
+
b.onLoad({ filter: /\.[cm]?[jt]sx?$/ }, async (a) => {
|
|
72
|
+
const ext = a.path.slice(a.path.lastIndexOf(".") + 1);
|
|
73
|
+
const loader = ext === "tsx" || ext === "jsx" ? ext : ext.endsWith("ts") ? "ts" : "js";
|
|
74
|
+
const text = await Bun.file(a.path).text();
|
|
75
|
+
// A macro import runs at build time, in the builder: a baba has no build-time OS either.
|
|
76
|
+
if (macro.test(text)) throw new Error(`${where(a.path)} imports a macro (with { type: "macro" }); a baba has no build step of its own: compute at run time, in an effect (see README.md)`);
|
|
77
|
+
// A file that does not parse is the bundler's to report, with its position.
|
|
78
|
+
let imports: { path: string }[];
|
|
79
|
+
try { imports = new Bun.Transpiler({ loader }).scanImports(text); } catch { return undefined; }
|
|
80
|
+
const bad = imports.find((i) => filter.test(i.path));
|
|
81
|
+
if (bad) throw refuse(a.path, bad.path);
|
|
82
|
+
return undefined;
|
|
83
|
+
});
|
|
84
|
+
},
|
|
85
|
+
};
|
package/src/build/mdx.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// MDX is compiled in the builder. Only the resulting prompt tree reaches the runtime.
|
|
2
|
+
import { compile } from "@mdx-js/mdx";
|
|
3
|
+
import gfm from "remark-gfm";
|
|
4
|
+
import { forbiddenModules, advice } from "./guard.ts";
|
|
5
|
+
|
|
6
|
+
function promptExport() {
|
|
7
|
+
return (tree: any) => {
|
|
8
|
+
const metadata = tree.body.some((n: any) => n.type === "ExportNamedDeclaration" && n.declaration?.type === "VariableDeclaration" && n.declaration.declarations.some((d: any) => d.id.name === "prompt"));
|
|
9
|
+
if (!metadata) throw new Error('MDX promptware needs an exported prompt metadata object');
|
|
10
|
+
const exported = tree.body.find((n: any) => n.type === "ExportDefaultDeclaration");
|
|
11
|
+
if (!exported || exported.declaration.type !== "FunctionDeclaration") throw new Error("MDX must export a document component");
|
|
12
|
+
const component = exported.declaration;
|
|
13
|
+
const id = (name: string) => ({ type: "Identifier", name });
|
|
14
|
+
const literal = (value: string) => ({ type: "Literal", value });
|
|
15
|
+
tree.body[tree.body.indexOf(exported)] = component;
|
|
16
|
+
tree.body.push({ type: "ImportDeclaration", source: literal("babavoss/prompt"), specifiers: [{ type: "ImportSpecifier", imported: id("mdxDocument"), local: id("$promptDocument") }] });
|
|
17
|
+
tree.body.push({ type: "ExportDefaultDeclaration", declaration: { type: "CallExpression", callee: id("$promptDocument"), arguments: [id("prompt"), id(component.id.name)], optional: false } });
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function compilePromptMdx(value: string, path: string): Promise<string> {
|
|
22
|
+
const result = String(await compile({ value, path }, {
|
|
23
|
+
jsxImportSource: "babavoss/prompt", format: "mdx", development: false,
|
|
24
|
+
remarkPlugins: [gfm], recmaPlugins: [promptExport],
|
|
25
|
+
}));
|
|
26
|
+
// Bun's bun-target builtin imports can bypass onResolve: apply the same guard after MDX compilation.
|
|
27
|
+
for (const item of new Bun.Transpiler({ loader: "js" }).scanImports(result)) {
|
|
28
|
+
if (forbiddenModules[item.path]) throw new Error(`${path} imports ${item.path}; ${advice(forbiddenModules[item.path]!)}`);
|
|
29
|
+
}
|
|
30
|
+
if (/\b(?:with|assert)\s*\{\s*type\s*:\s*["']macro["']\s*\}/.test(result)) throw new Error(`${path}: macros are not allowed in promptware`);
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function promptMdx(files: Record<string, string> = {}): import("bun").BunPlugin {
|
|
35
|
+
return { name: "babavoss-prompt-mdx", setup(b) {
|
|
36
|
+
b.onLoad({ filter: /\.mdx$/ }, async ({ path }) => ({ contents: await compilePromptMdx(files[path] ?? await Bun.file(path).text(), path), loader: "js" }));
|
|
37
|
+
} };
|
|
38
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { promptMdx } from "./mdx.ts";
|
|
2
|
+
// A baba's backend as one text: its entry, the framework it imports and the
|
|
3
|
+
// worker harness, bundled for the runtime, which has no file access. The
|
|
4
|
+
// entry is virtual: it imports the baba and hands it to the harness, so the
|
|
5
|
+
// text runs as a Worker.
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import type { Project } from "../baba/project.ts";
|
|
8
|
+
import { aliasOf, loadBuildPlugins, resolver } from "./views.ts";
|
|
9
|
+
import { framework } from "../kernel/where.ts";
|
|
10
|
+
|
|
11
|
+
const harness = join(framework, "src/runtime/worker.ts");
|
|
12
|
+
const ENTRY = "/babavoss/entry.ts";
|
|
13
|
+
|
|
14
|
+
export async function bundleProject(project: Project): Promise<string> {
|
|
15
|
+
const text = [
|
|
16
|
+
`import v from ${JSON.stringify(project.entry)};`,
|
|
17
|
+
`import { serve } from ${JSON.stringify(harness)};`,
|
|
18
|
+
`export default v;`,
|
|
19
|
+
`serve(v);`,
|
|
20
|
+
].join("\n") + "\n";
|
|
21
|
+
const extra = project.build.plugins.length ? await loadBuildPlugins(project.build.plugins, project.baba, project.baba) : [];
|
|
22
|
+
const r = await Bun.build({
|
|
23
|
+
entrypoints: [ENTRY],
|
|
24
|
+
files: { [ENTRY]: text },
|
|
25
|
+
target: "bun", format: "esm",
|
|
26
|
+
plugins: [promptMdx(), resolver, aliasOf(project.baba), ...extra],
|
|
27
|
+
define: { "process.env.NODE_ENV": '"production"' },
|
|
28
|
+
throw: false,
|
|
29
|
+
});
|
|
30
|
+
if (!r.success) throw new Error(r.logs.filter((l) => l.level === "error").map(explain).join("\n"));
|
|
31
|
+
return await r.outputs[0]!.text();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** One build error as the editor would show it: where, then what. */
|
|
35
|
+
function explain(l: Awaited<ReturnType<typeof Bun.build>>["logs"][number]): string {
|
|
36
|
+
const at = l.position ? `${l.position.file}:${l.position.line}:${l.position.column}: ` : "";
|
|
37
|
+
return `${at}${l.message}`;
|
|
38
|
+
}
|