lakebed 0.0.1
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/README.md +94 -0
- package/package.json +62 -0
- package/src/anonymous-server.js +1078 -0
- package/src/anonymous.js +996 -0
- package/src/cli.js +1066 -0
- package/src/client.d.ts +8 -0
- package/src/client.js +154 -0
- package/src/runtime.js +252 -0
- package/src/server.d.ts +53 -0
- package/src/server.js +39 -0
- package/src/source-store.js +110 -0
package/src/cli.js
ADDED
|
@@ -0,0 +1,1066 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createServer } from "node:http";
|
|
3
|
+
import { existsSync, realpathSync } from "node:fs";
|
|
4
|
+
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
5
|
+
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
6
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
7
|
+
import * as esbuild from "esbuild";
|
|
8
|
+
import { WebSocketServer } from "ws";
|
|
9
|
+
import {
|
|
10
|
+
ANONYMOUS_ARTIFACT_MEDIA_TYPE,
|
|
11
|
+
AnonymousCompilerError,
|
|
12
|
+
createAnonymousArtifact,
|
|
13
|
+
parseTtlSeconds,
|
|
14
|
+
stableStringify
|
|
15
|
+
} from "./anonymous.js";
|
|
16
|
+
import { startAnonymousServer } from "./anonymous-server.js";
|
|
17
|
+
import { LogBuffer, StateCell } from "./runtime.js";
|
|
18
|
+
import { createMemorySourceStoreFromDirectory, sourcePathDirname, sourcePathJoin } from "./source-store.js";
|
|
19
|
+
|
|
20
|
+
const root = process.cwd();
|
|
21
|
+
const packageDir = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
22
|
+
const packageNodeModules = resolve(packageDir, "node_modules");
|
|
23
|
+
const sourceNamespace = "lakebed-source";
|
|
24
|
+
|
|
25
|
+
function usage() {
|
|
26
|
+
console.log(`lakebed
|
|
27
|
+
|
|
28
|
+
Usage:
|
|
29
|
+
lakebed new <name> [--template todo]
|
|
30
|
+
lakebed dev <capsule-dir> [--port 3000]
|
|
31
|
+
lakebed build <capsule-dir> --target anonymous [--out .lakebed/artifacts/app.json] [--json]
|
|
32
|
+
lakebed deploy <capsule-dir> [--ttl 7d] [--api <url>] [--json]
|
|
33
|
+
lakebed anonymous-server [--port 8787] [--public-root-url <url>]
|
|
34
|
+
lakebed inspect <deploy-id-or-url> [--api <url>] [--json]
|
|
35
|
+
lakebed run-many <capsule-dir> [--count 20] [--base-port 4000]
|
|
36
|
+
lakebed auth as <name>
|
|
37
|
+
lakebed auth reset
|
|
38
|
+
lakebed db list [deploy-id-or-url] [--port 3000]
|
|
39
|
+
lakebed db dump [deploy-id-or-url] [--port 3000]
|
|
40
|
+
lakebed logs [deploy-id-or-url] [--port 3000]
|
|
41
|
+
`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function readArg(args, name, fallback) {
|
|
45
|
+
const index = args.indexOf(name);
|
|
46
|
+
if (index === -1) {
|
|
47
|
+
return fallback;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return args[index + 1] ?? fallback;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function positionals(args) {
|
|
54
|
+
const values = [];
|
|
55
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
56
|
+
const value = args[index];
|
|
57
|
+
if (value.startsWith("--")) {
|
|
58
|
+
index += 1;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
values.push(value);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return values;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function readNumberArg(args, name, fallback) {
|
|
69
|
+
const value = Number(readArg(args, name, String(fallback)));
|
|
70
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
71
|
+
throw new Error(`${name} must be a positive integer.`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return value;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function hasFlag(args, name) {
|
|
78
|
+
return args.includes(name);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function resolveCapsuleDir(value) {
|
|
82
|
+
if (!value) {
|
|
83
|
+
return resolve(root, "examples/todo");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return isAbsolute(value) ? value : resolve(root, value);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function authFile() {
|
|
90
|
+
return resolve(root, ".lakebed/auth.json");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function toGuestName(name) {
|
|
94
|
+
return String(name ?? "local")
|
|
95
|
+
.replace(/^guest:/, "")
|
|
96
|
+
.trim()
|
|
97
|
+
.replace(/[^a-zA-Z0-9_.-]+/g, "-")
|
|
98
|
+
.replace(/^-+|-+$/g, "")
|
|
99
|
+
.toLowerCase() || "local";
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function toDisplayName(name) {
|
|
103
|
+
return toGuestName(name)
|
|
104
|
+
.split(/[-_\s.]+/)
|
|
105
|
+
.filter(Boolean)
|
|
106
|
+
.map((part) => `${part[0]?.toUpperCase() ?? ""}${part.slice(1)}`)
|
|
107
|
+
.join(" ");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function createGuestAuth(name) {
|
|
111
|
+
const guestName = toGuestName(name);
|
|
112
|
+
return {
|
|
113
|
+
userId: `guest:${guestName}`,
|
|
114
|
+
displayName: toDisplayName(guestName)
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function readAuth() {
|
|
119
|
+
try {
|
|
120
|
+
return JSON.parse(await readFile(authFile(), "utf8"));
|
|
121
|
+
} catch {
|
|
122
|
+
return createGuestAuth("local");
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function writeAuth(auth) {
|
|
127
|
+
await mkdir(dirname(authFile()), { recursive: true });
|
|
128
|
+
await writeFile(authFile(), `${JSON.stringify(auth, null, 2)}\n`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function authFromUrl(url, defaultAuth) {
|
|
132
|
+
const guestName = url.searchParams.get("lakebed_guest") ?? url.searchParams.get("guest");
|
|
133
|
+
return guestName ? createGuestAuth(guestName) : defaultAuth;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function isBareSpecifier(path) {
|
|
137
|
+
return !path.startsWith(".") && !path.startsWith("/") && !/^[a-zA-Z]:/.test(path);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function resolveSourceFile(sourceStore, requestedPath) {
|
|
141
|
+
const normalized = sourcePathJoin(requestedPath);
|
|
142
|
+
const candidates = [
|
|
143
|
+
normalized,
|
|
144
|
+
`${normalized}.ts`,
|
|
145
|
+
`${normalized}.tsx`,
|
|
146
|
+
`${normalized}.js`,
|
|
147
|
+
`${normalized}.jsx`,
|
|
148
|
+
`${normalized}.json`,
|
|
149
|
+
sourcePathJoin(normalized, "index.ts"),
|
|
150
|
+
sourcePathJoin(normalized, "index.tsx"),
|
|
151
|
+
sourcePathJoin(normalized, "index.js"),
|
|
152
|
+
sourcePathJoin(normalized, "index.jsx")
|
|
153
|
+
];
|
|
154
|
+
|
|
155
|
+
for (const candidate of candidates) {
|
|
156
|
+
if (sourceStore.hasFile(candidate)) {
|
|
157
|
+
return candidate;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
throw new Error(`Unable to resolve source import: ${requestedPath}`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function loaderForPath(path) {
|
|
165
|
+
if (path.endsWith(".tsx")) {
|
|
166
|
+
return "tsx";
|
|
167
|
+
}
|
|
168
|
+
if (path.endsWith(".ts")) {
|
|
169
|
+
return "ts";
|
|
170
|
+
}
|
|
171
|
+
if (path.endsWith(".jsx")) {
|
|
172
|
+
return "jsx";
|
|
173
|
+
}
|
|
174
|
+
if (path.endsWith(".json")) {
|
|
175
|
+
return "json";
|
|
176
|
+
}
|
|
177
|
+
return "js";
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function createSourcePlugin(sourceStore, target) {
|
|
181
|
+
const allowedBare = new Set(
|
|
182
|
+
target === "server"
|
|
183
|
+
? ["lakebed/server"]
|
|
184
|
+
: ["lakebed/client", "preact", "preact/hooks", "preact/jsx-runtime", "preact/jsx-dev-runtime"]
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
name: "lakebed-source-store",
|
|
189
|
+
setup(build) {
|
|
190
|
+
build.onResolve({ filter: /.*/ }, async (args) => {
|
|
191
|
+
if (args.kind === "entry-point") {
|
|
192
|
+
return {
|
|
193
|
+
path: await resolveSourceFile(sourceStore, args.path),
|
|
194
|
+
namespace: sourceNamespace
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (args.namespace !== sourceNamespace) {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (args.path.startsWith("node:")) {
|
|
203
|
+
return {
|
|
204
|
+
errors: [{ text: `Node built-ins are not available inside Lakebed ${target} modules: ${args.path}` }]
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (isBareSpecifier(args.path)) {
|
|
209
|
+
if (allowedBare.has(args.path) || (target === "client" && args.path.startsWith("preact/"))) {
|
|
210
|
+
if (target === "server" && args.path === "lakebed/server") {
|
|
211
|
+
return { path: join(packageDir, "src/server.js") };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (target === "server") {
|
|
215
|
+
return { path: args.path, external: true };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return build.resolve(args.path, {
|
|
219
|
+
kind: args.kind,
|
|
220
|
+
resolveDir: packageDir
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
errors: [
|
|
226
|
+
{
|
|
227
|
+
text: `External packages are not supported in Lakebed v0: ${args.path}. Use relative files or Lakebed built-ins.`
|
|
228
|
+
}
|
|
229
|
+
]
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const basePath = args.path.startsWith("/") ? "" : sourcePathDirname(args.importer);
|
|
234
|
+
return {
|
|
235
|
+
path: await resolveSourceFile(sourceStore, sourcePathJoin(basePath, args.path)),
|
|
236
|
+
namespace: sourceNamespace
|
|
237
|
+
};
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
build.onLoad({ filter: /.*/, namespace: sourceNamespace }, async (args) => ({
|
|
241
|
+
contents: await sourceStore.readFile(args.path),
|
|
242
|
+
loader: loaderForPath(args.path),
|
|
243
|
+
resolveDir: sourcePathDirname(args.path)
|
|
244
|
+
}));
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async function readServerEnv(sourceStore) {
|
|
250
|
+
const env = { ...process.env };
|
|
251
|
+
if (!sourceStore.hasFile(".env.lakebed.server")) {
|
|
252
|
+
return env;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const contents = await sourceStore.readFile(".env.lakebed.server");
|
|
256
|
+
for (const rawLine of contents.split(/\r?\n/)) {
|
|
257
|
+
const line = rawLine.trim();
|
|
258
|
+
if (!line || line.startsWith("#")) {
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const match = line.match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
|
|
263
|
+
if (!match) {
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const [, key, rawValue] = match;
|
|
268
|
+
env[key] = rawValue.replace(/^['"]|['"]$/g, "");
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return env;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export async function buildCapsule({ capsuleDir, sourceStore, capsuleId = "dev" } = {}) {
|
|
275
|
+
const resolvedCapsuleDir = resolveCapsuleDir(capsuleDir);
|
|
276
|
+
const originalStore = sourceStore ?? (await createMemorySourceStoreFromDirectory(resolvedCapsuleDir));
|
|
277
|
+
const workingStore = originalStore.clone();
|
|
278
|
+
const buildDir = resolve(root, ".lakebed/build", capsuleId);
|
|
279
|
+
|
|
280
|
+
await rm(buildDir, { recursive: true, force: true });
|
|
281
|
+
await mkdir(buildDir, { recursive: true });
|
|
282
|
+
|
|
283
|
+
if (!workingStore.hasFile("server/index.ts")) {
|
|
284
|
+
throw new Error("Missing capsule entry: server/index.ts");
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (!workingStore.hasFile("client/index.tsx")) {
|
|
288
|
+
throw new Error("Missing client entry: client/index.tsx");
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const serverOut = join(buildDir, "server.mjs");
|
|
292
|
+
const clientOut = join(buildDir, "client.js");
|
|
293
|
+
|
|
294
|
+
await workingStore.writeFile(
|
|
295
|
+
"__lakebed/client-entry.tsx",
|
|
296
|
+
`import { h, render } from "preact";\nimport { App } from "../client/index.tsx";\n\nrender(h(App, {}), document.getElementById("app"));\n`
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
await esbuild.build({
|
|
300
|
+
entryPoints: ["server/index.ts"],
|
|
301
|
+
outfile: serverOut,
|
|
302
|
+
bundle: true,
|
|
303
|
+
platform: "node",
|
|
304
|
+
format: "esm",
|
|
305
|
+
sourcemap: "inline",
|
|
306
|
+
jsx: "automatic",
|
|
307
|
+
jsxImportSource: "preact",
|
|
308
|
+
plugins: [createSourcePlugin(workingStore, "server")]
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
await esbuild.build({
|
|
312
|
+
entryPoints: ["__lakebed/client-entry.tsx"],
|
|
313
|
+
outfile: clientOut,
|
|
314
|
+
bundle: true,
|
|
315
|
+
platform: "browser",
|
|
316
|
+
format: "esm",
|
|
317
|
+
sourcemap: "inline",
|
|
318
|
+
jsx: "automatic",
|
|
319
|
+
jsxImportSource: "preact",
|
|
320
|
+
nodePaths: [packageNodeModules],
|
|
321
|
+
plugins: [createSourcePlugin(workingStore, "client")]
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
const capsuleModule = await import(`${pathToFileURL(serverOut).href}?t=${Date.now()}-${Math.random()}`);
|
|
325
|
+
return {
|
|
326
|
+
app: capsuleModule.default,
|
|
327
|
+
buildDir,
|
|
328
|
+
clientOut,
|
|
329
|
+
env: await readServerEnv(workingStore),
|
|
330
|
+
sourceStore: workingStore
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function html(title) {
|
|
335
|
+
return `<!doctype html>
|
|
336
|
+
<html lang="en">
|
|
337
|
+
<head>
|
|
338
|
+
<meta charset="utf-8" />
|
|
339
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
340
|
+
<title>${title}</title>
|
|
341
|
+
</head>
|
|
342
|
+
<body>
|
|
343
|
+
<div id="app"></div>
|
|
344
|
+
<script type="module" src="/client.js"></script>
|
|
345
|
+
<script>
|
|
346
|
+
const tailwind = document.createElement("script");
|
|
347
|
+
tailwind.src = "https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4";
|
|
348
|
+
tailwind.async = true;
|
|
349
|
+
document.head.appendChild(tailwind);
|
|
350
|
+
</script>
|
|
351
|
+
</body>
|
|
352
|
+
</html>`;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function sendJson(ws, message) {
|
|
356
|
+
ws.send(JSON.stringify(message));
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function createContext({ stateCell, auth, logs, env }) {
|
|
360
|
+
return {
|
|
361
|
+
auth,
|
|
362
|
+
db: stateCell.createDb(),
|
|
363
|
+
env,
|
|
364
|
+
log: logs.createLogger()
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
async function runQuery({ app, stateCell, auth, logs, env, name }) {
|
|
369
|
+
const handler = app.queries?.[name];
|
|
370
|
+
if (!handler) {
|
|
371
|
+
throw new Error(`Unknown query: ${name}`);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return handler(createContext({ stateCell, auth, logs, env }));
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
async function runMutation({ app, stateCell, auth, logs, env, name, args }) {
|
|
378
|
+
const handler = app.mutations?.[name];
|
|
379
|
+
if (!handler) {
|
|
380
|
+
throw new Error(`Unknown mutation: ${name}`);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return stateCell.transaction((db) =>
|
|
384
|
+
handler(
|
|
385
|
+
{
|
|
386
|
+
auth,
|
|
387
|
+
db,
|
|
388
|
+
env,
|
|
389
|
+
log: logs.createLogger()
|
|
390
|
+
},
|
|
391
|
+
...args
|
|
392
|
+
)
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
export async function startDevServer({ capsuleDir, sourceStore, port = 3000, capsuleId = "dev", quiet = false } = {}) {
|
|
397
|
+
const resolvedCapsuleDir = resolveCapsuleDir(capsuleDir);
|
|
398
|
+
const built = await buildCapsule({ capsuleDir: resolvedCapsuleDir, sourceStore, capsuleId });
|
|
399
|
+
const defaultAuth = await readAuth();
|
|
400
|
+
const stateCell = new StateCell(built.app.schema);
|
|
401
|
+
const logs = new LogBuffer();
|
|
402
|
+
const subscriptions = new Map();
|
|
403
|
+
|
|
404
|
+
const server = createServer(async (req, res) => {
|
|
405
|
+
try {
|
|
406
|
+
const requestUrl = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
|
407
|
+
|
|
408
|
+
if (requestUrl.pathname === "/" || requestUrl.pathname === "/index.html") {
|
|
409
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
410
|
+
res.end(html(built.app.name ?? "Lakebed Capsule"));
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (requestUrl.pathname === "/client.js") {
|
|
415
|
+
res.writeHead(200, { "Content-Type": "application/javascript; charset=utf-8" });
|
|
416
|
+
res.end(await readFile(built.clientOut, "utf8"));
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if (requestUrl.pathname === "/__lakebed/logs") {
|
|
421
|
+
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
|
|
422
|
+
res.end(JSON.stringify(logs.entries, null, 2));
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (requestUrl.pathname === "/__lakebed/db/tables") {
|
|
427
|
+
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
|
|
428
|
+
res.end(JSON.stringify(stateCell.listTables(), null, 2));
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (requestUrl.pathname === "/__lakebed/db") {
|
|
433
|
+
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
|
|
434
|
+
res.end(JSON.stringify(stateCell.dump(), null, 2));
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
res.writeHead(404);
|
|
439
|
+
res.end("Not found");
|
|
440
|
+
} catch (error) {
|
|
441
|
+
res.writeHead(500);
|
|
442
|
+
res.end(error instanceof Error ? error.stack : String(error));
|
|
443
|
+
}
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
const wss = new WebSocketServer({ noServer: true });
|
|
447
|
+
|
|
448
|
+
async function publishAll() {
|
|
449
|
+
for (const [ws, subscription] of subscriptions) {
|
|
450
|
+
for (const name of subscription.queries) {
|
|
451
|
+
try {
|
|
452
|
+
const data = await runQuery({
|
|
453
|
+
app: built.app,
|
|
454
|
+
stateCell,
|
|
455
|
+
auth: subscription.auth,
|
|
456
|
+
logs,
|
|
457
|
+
env: built.env,
|
|
458
|
+
name
|
|
459
|
+
});
|
|
460
|
+
sendJson(ws, { op: "query.result", name, data });
|
|
461
|
+
} catch (error) {
|
|
462
|
+
sendJson(ws, { op: "query.error", name, error: error instanceof Error ? error.message : String(error) });
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
wss.on("connection", (ws, _req, auth) => {
|
|
469
|
+
subscriptions.set(ws, { auth, queries: new Set() });
|
|
470
|
+
sendJson(ws, { op: "auth.result", auth });
|
|
471
|
+
|
|
472
|
+
ws.on("message", async (raw) => {
|
|
473
|
+
const message = JSON.parse(String(raw));
|
|
474
|
+
const subscription = subscriptions.get(ws);
|
|
475
|
+
|
|
476
|
+
try {
|
|
477
|
+
if (!subscription) {
|
|
478
|
+
throw new Error("Lakebed connection closed.");
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
if (message.op === "auth.get") {
|
|
482
|
+
sendJson(ws, { id: message.id, op: "auth.result", ok: true, auth: subscription.auth });
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
if (message.op === "query.subscribe") {
|
|
487
|
+
subscription.queries.add(message.name);
|
|
488
|
+
const data = await runQuery({
|
|
489
|
+
app: built.app,
|
|
490
|
+
stateCell,
|
|
491
|
+
auth: subscription.auth,
|
|
492
|
+
logs,
|
|
493
|
+
env: built.env,
|
|
494
|
+
name: message.name
|
|
495
|
+
});
|
|
496
|
+
sendJson(ws, { id: message.id, op: "query.result", ok: true, name: message.name, data });
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
if (message.op === "mutation.run") {
|
|
501
|
+
const { result } = await runMutation({
|
|
502
|
+
app: built.app,
|
|
503
|
+
stateCell,
|
|
504
|
+
auth: subscription.auth,
|
|
505
|
+
logs,
|
|
506
|
+
env: built.env,
|
|
507
|
+
name: message.name,
|
|
508
|
+
args: message.args ?? []
|
|
509
|
+
});
|
|
510
|
+
sendJson(ws, { id: message.id, op: "mutation.result", ok: true, result });
|
|
511
|
+
await publishAll();
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
throw new Error(`Unknown operation: ${message.op}`);
|
|
516
|
+
} catch (error) {
|
|
517
|
+
sendJson(ws, {
|
|
518
|
+
id: message.id,
|
|
519
|
+
op: "error",
|
|
520
|
+
ok: false,
|
|
521
|
+
error: error instanceof Error ? error.message : String(error)
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
ws.on("close", () => {
|
|
527
|
+
subscriptions.delete(ws);
|
|
528
|
+
});
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
server.on("upgrade", (req, socket, head) => {
|
|
532
|
+
const requestUrl = new URL(req.url ?? "/", "http://lakebed.local");
|
|
533
|
+
if (requestUrl.pathname !== "/__lakebed/ws") {
|
|
534
|
+
socket.destroy();
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
const auth = authFromUrl(requestUrl, defaultAuth);
|
|
539
|
+
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
540
|
+
wss.emit("connection", ws, req, auth);
|
|
541
|
+
});
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
await new Promise((resolveListen, rejectListen) => {
|
|
545
|
+
server.once("error", rejectListen);
|
|
546
|
+
server.listen(port, () => {
|
|
547
|
+
server.off("error", rejectListen);
|
|
548
|
+
resolveListen();
|
|
549
|
+
});
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
if (!quiet) {
|
|
553
|
+
console.log(`Lakebed capsule running at http://localhost:${port}`);
|
|
554
|
+
console.log(`Capsule: ${resolvedCapsuleDir}`);
|
|
555
|
+
console.log(`Auth: ${defaultAuth.userId}`);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
return {
|
|
559
|
+
app: built.app,
|
|
560
|
+
buildDir: built.buildDir,
|
|
561
|
+
capsuleDir: resolvedCapsuleDir,
|
|
562
|
+
logs,
|
|
563
|
+
port,
|
|
564
|
+
stateCell,
|
|
565
|
+
url: `http://localhost:${port}`,
|
|
566
|
+
async close() {
|
|
567
|
+
for (const client of wss.clients) {
|
|
568
|
+
client.close();
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
await new Promise((resolveClose) => {
|
|
572
|
+
wss.close(() => {
|
|
573
|
+
server.close(() => resolveClose());
|
|
574
|
+
});
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
async function dev(args) {
|
|
581
|
+
const [capsuleArg] = positionals(args);
|
|
582
|
+
const port = readNumberArg(args, "--port", 3000);
|
|
583
|
+
await startDevServer({
|
|
584
|
+
capsuleDir: resolveCapsuleDir(capsuleArg),
|
|
585
|
+
port,
|
|
586
|
+
capsuleId: `dev-${port}`
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
async function buildAnonymousEnvelope(capsuleArg) {
|
|
591
|
+
const capsuleDir = resolveCapsuleDir(capsuleArg);
|
|
592
|
+
const sourceStore = await createMemorySourceStoreFromDirectory(capsuleDir);
|
|
593
|
+
const built = await buildCapsule({
|
|
594
|
+
capsuleDir,
|
|
595
|
+
capsuleId: `anonymous-${Date.now()}`,
|
|
596
|
+
sourceStore
|
|
597
|
+
});
|
|
598
|
+
const artifact = await createAnonymousArtifact({
|
|
599
|
+
app: built.app,
|
|
600
|
+
clientOut: built.clientOut,
|
|
601
|
+
sourceStore
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
return {
|
|
605
|
+
artifact: artifact.artifact,
|
|
606
|
+
artifactHash: artifact.artifactHash,
|
|
607
|
+
clientBundle: artifact.clientBundle,
|
|
608
|
+
clientBundleHash: artifact.clientBundleHash,
|
|
609
|
+
mediaType: ANONYMOUS_ARTIFACT_MEDIA_TYPE
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function defaultArtifactPath(capsuleDir) {
|
|
614
|
+
return resolve(root, ".lakebed/artifacts", `${basename(capsuleDir)}.anonymous.json`);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
async function buildCommand(args) {
|
|
618
|
+
const [capsuleArg] = positionals(args);
|
|
619
|
+
const target = readArg(args, "--target", "anonymous");
|
|
620
|
+
if (target !== "anonymous") {
|
|
621
|
+
throw new Error(`Unsupported build target: ${target}. The only explicit build target today is anonymous.`);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
const capsuleDir = resolveCapsuleDir(capsuleArg);
|
|
625
|
+
const out = readArg(args, "--out", defaultArtifactPath(capsuleDir));
|
|
626
|
+
const envelope = await buildAnonymousEnvelope(capsuleArg);
|
|
627
|
+
await mkdir(dirname(out), { recursive: true });
|
|
628
|
+
await writeFile(out, `${stableStringify(envelope)}\n`);
|
|
629
|
+
|
|
630
|
+
if (hasFlag(args, "--json")) {
|
|
631
|
+
console.log(
|
|
632
|
+
JSON.stringify(
|
|
633
|
+
{
|
|
634
|
+
artifactHash: envelope.artifactHash,
|
|
635
|
+
artifactPath: out,
|
|
636
|
+
clientBundleHash: envelope.clientBundleHash,
|
|
637
|
+
format: envelope.artifact.format
|
|
638
|
+
},
|
|
639
|
+
null,
|
|
640
|
+
2
|
|
641
|
+
)
|
|
642
|
+
);
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
console.log(`Anonymous artifact written to ${out}`);
|
|
647
|
+
console.log(`Artifact: ${envelope.artifactHash}`);
|
|
648
|
+
console.log(`Client: ${envelope.clientBundleHash}`);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function deployApiUrl(args) {
|
|
652
|
+
return String(readArg(args, "--api", process.env.LAKEBED_DEPLOY_API ?? process.env.SPAN_DEPLOY_API ?? "http://localhost:8787")).replace(
|
|
653
|
+
/\/+$/g,
|
|
654
|
+
""
|
|
655
|
+
);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
async function readResponseJson(response) {
|
|
659
|
+
const body = await response.text();
|
|
660
|
+
if (!response.ok) {
|
|
661
|
+
throw new Error(body || `Request failed with ${response.status}`);
|
|
662
|
+
}
|
|
663
|
+
return JSON.parse(body);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
async function deployCommand(args) {
|
|
667
|
+
const [capsuleArg] = positionals(args);
|
|
668
|
+
const envelope = await buildAnonymousEnvelope(capsuleArg);
|
|
669
|
+
const ttl = parseTtlSeconds(readArg(args, "--ttl", "7d"));
|
|
670
|
+
const api = deployApiUrl(args);
|
|
671
|
+
const response = await fetch(`${api}/v1/anonymous-deploys`, {
|
|
672
|
+
body: JSON.stringify({
|
|
673
|
+
artifact: envelope.artifact,
|
|
674
|
+
clientBundle: envelope.clientBundle,
|
|
675
|
+
clientVersion: "0.0.1",
|
|
676
|
+
requestedTtlSeconds: ttl
|
|
677
|
+
}),
|
|
678
|
+
headers: {
|
|
679
|
+
"Content-Type": "application/json"
|
|
680
|
+
},
|
|
681
|
+
method: "POST"
|
|
682
|
+
});
|
|
683
|
+
const deployed = await readResponseJson(response);
|
|
684
|
+
|
|
685
|
+
if (hasFlag(args, "--json")) {
|
|
686
|
+
console.log(JSON.stringify(deployed, null, 2));
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
console.log("Deploying anonymous preview...\n");
|
|
691
|
+
console.log(`App: ${deployed.url}`);
|
|
692
|
+
console.log(`Expires: ${deployed.expiresAt}`);
|
|
693
|
+
console.log(`Claim: ${deployed.claimUrl}`);
|
|
694
|
+
console.log(`Inspect: lakebed inspect ${deployed.deployId}`);
|
|
695
|
+
console.log("\nLimits:");
|
|
696
|
+
console.log(` source/artifact: ${deployed.limits.artifactBytes} bytes`);
|
|
697
|
+
console.log(` state: ${deployed.limits.stateBytes} bytes`);
|
|
698
|
+
console.log(` requests: ${deployed.limits.requestsPerDay} / day`);
|
|
699
|
+
console.log(` mutations: ${deployed.limits.mutationsPerDay} / day`);
|
|
700
|
+
console.log(" outbound fetch: disabled");
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
async function anonymousServerCommand(args) {
|
|
704
|
+
const port = readNumberArg(args, "--port", Number(process.env.PORT ?? 8787));
|
|
705
|
+
await startAnonymousServer({
|
|
706
|
+
appBaseDomain: readArg(args, "--app-base-domain", process.env.LAKEBED_APP_BASE_DOMAIN ?? ""),
|
|
707
|
+
port,
|
|
708
|
+
publicRootUrl: readArg(args, "--public-root-url", process.env.PUBLIC_ROOT_URL ?? `http://localhost:${port}`)
|
|
709
|
+
});
|
|
710
|
+
await new Promise(() => {});
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
async function resolveDeployUrl(target, args) {
|
|
714
|
+
if (!target) {
|
|
715
|
+
throw new Error("Expected a deploy ID or URL.");
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
try {
|
|
719
|
+
const url = new URL(target);
|
|
720
|
+
return url.href.replace(/\/+$/g, "");
|
|
721
|
+
} catch {
|
|
722
|
+
const api = deployApiUrl(args);
|
|
723
|
+
const response = await fetch(`${api}/v1/deploys/${encodeURIComponent(target)}`);
|
|
724
|
+
const deploy = await readResponseJson(response);
|
|
725
|
+
return deploy.url.replace(/\/+$/g, "");
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
async function hostedJson(target, path, args) {
|
|
730
|
+
const url = await resolveDeployUrl(target, args);
|
|
731
|
+
const response = await fetch(`${url}${path}`);
|
|
732
|
+
return readResponseJson(response);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
async function inspectCommand(args) {
|
|
736
|
+
const [target] = positionals(args);
|
|
737
|
+
const manifest = await hostedJson(target, "/__lakebed/manifest", args);
|
|
738
|
+
|
|
739
|
+
if (hasFlag(args, "--json")) {
|
|
740
|
+
console.log(JSON.stringify(manifest, null, 2));
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
console.log(`Deploy: ${manifest.deployId}`);
|
|
745
|
+
console.log(`URL: ${manifest.url}`);
|
|
746
|
+
console.log(`Expires: ${manifest.expiresAt}`);
|
|
747
|
+
console.log(`Artifact: ${manifest.artifactHash}`);
|
|
748
|
+
console.log(`Queries: ${manifest.queries.join(", ") || "(none)"}`);
|
|
749
|
+
console.log(`Mutations: ${manifest.mutations.join(", ") || "(none)"}`);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
async function authCommand(args) {
|
|
753
|
+
if (args[0] === "as" && args[1]) {
|
|
754
|
+
const auth = createGuestAuth(args[1]);
|
|
755
|
+
await writeAuth(auth);
|
|
756
|
+
console.log(`Lakebed auth set to ${auth.userId}`);
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
if (args[0] === "reset") {
|
|
761
|
+
await writeAuth(createGuestAuth("local"));
|
|
762
|
+
console.log("Lakebed auth reset to guest:local");
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
usage();
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
async function fetchLakebedJson(port, path) {
|
|
770
|
+
const response = await fetch(`http://localhost:${port}${path}`);
|
|
771
|
+
if (!response.ok) {
|
|
772
|
+
throw new Error(`Unable to read ${path} from port ${port}: ${response.status}`);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
return response.text();
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
async function logsCommand(args) {
|
|
779
|
+
const [target] = positionals(args);
|
|
780
|
+
if (target) {
|
|
781
|
+
console.log(JSON.stringify(await hostedJson(target, "/__lakebed/logs", args), null, 2));
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
const port = readNumberArg(args, "--port", 3000);
|
|
786
|
+
console.log(await fetchLakebedJson(port, "/__lakebed/logs"));
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
async function dbCommand(args) {
|
|
790
|
+
const [subcommand, target] = positionals(args);
|
|
791
|
+
const port = readNumberArg(args, "--port", 3000);
|
|
792
|
+
|
|
793
|
+
if (subcommand === "list") {
|
|
794
|
+
if (target) {
|
|
795
|
+
console.log(JSON.stringify(await hostedJson(target, "/__lakebed/db/tables", args), null, 2));
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
console.log(await fetchLakebedJson(port, "/__lakebed/db/tables"));
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
if (subcommand === "dump") {
|
|
804
|
+
if (target) {
|
|
805
|
+
console.log(JSON.stringify(await hostedJson(target, "/__lakebed/db", args), null, 2));
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
console.log(await fetchLakebedJson(port, "/__lakebed/db"));
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
usage();
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
function todoTemplate(name) {
|
|
817
|
+
const title = basename(name);
|
|
818
|
+
return {
|
|
819
|
+
"server/index.ts": `import { boolean, capsule, mutation, query, string, table } from "lakebed/server";
|
|
820
|
+
import { cleanTodoText } from "../shared/todo";
|
|
821
|
+
|
|
822
|
+
export default capsule({
|
|
823
|
+
schema: {
|
|
824
|
+
todos: table({
|
|
825
|
+
text: string(),
|
|
826
|
+
done: boolean().default(false),
|
|
827
|
+
ownerId: string()
|
|
828
|
+
})
|
|
829
|
+
},
|
|
830
|
+
|
|
831
|
+
queries: {
|
|
832
|
+
todos: query((ctx) =>
|
|
833
|
+
ctx.db.todos
|
|
834
|
+
.where("ownerId", ctx.auth.userId)
|
|
835
|
+
.orderBy("createdAt", "desc")
|
|
836
|
+
.all()
|
|
837
|
+
)
|
|
838
|
+
},
|
|
839
|
+
|
|
840
|
+
mutations: {
|
|
841
|
+
addTodo: mutation((ctx, text: string) => {
|
|
842
|
+
const cleanText = cleanTodoText(text);
|
|
843
|
+
if (!cleanText) {
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
ctx.db.todos.insert({ text: cleanText, ownerId: ctx.auth.userId });
|
|
848
|
+
})
|
|
849
|
+
}
|
|
850
|
+
});
|
|
851
|
+
`,
|
|
852
|
+
"client/index.tsx": `import { useAuth, useMutation, useQuery } from "lakebed/client";
|
|
853
|
+
import { cleanTodoText, type Todo } from "../shared/todo";
|
|
854
|
+
|
|
855
|
+
export function App() {
|
|
856
|
+
const auth = useAuth();
|
|
857
|
+
const todos = useQuery<Todo[]>("todos");
|
|
858
|
+
const addTodo = useMutation<[text: string], void>("addTodo");
|
|
859
|
+
|
|
860
|
+
async function onSubmit(event: SubmitEvent) {
|
|
861
|
+
event.preventDefault();
|
|
862
|
+
const form = event.currentTarget as HTMLFormElement;
|
|
863
|
+
const data = new FormData(form);
|
|
864
|
+
const text = cleanTodoText(String(data.get("text") ?? ""));
|
|
865
|
+
if (!text) {
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
await addTodo(text);
|
|
870
|
+
form.reset();
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
return (
|
|
874
|
+
<main className="min-h-screen bg-black px-6 py-10 text-white">
|
|
875
|
+
<section className="mx-auto max-w-2xl">
|
|
876
|
+
<p className="mb-3 font-mono text-sm text-neutral-500">signed in as {auth.userId}</p>
|
|
877
|
+
<h1 className="mb-8 text-5xl font-bold tracking-tight">${title}</h1>
|
|
878
|
+
<form className="mb-8 flex gap-3" onSubmit={(event) => void onSubmit(event)}>
|
|
879
|
+
<input className="min-w-0 flex-1 border border-neutral-700 bg-black px-3 py-2 text-white outline-none focus:border-white" name="text" placeholder="Add a todo" />
|
|
880
|
+
<button className="border border-white px-4 py-2 font-medium" type="submit">Add</button>
|
|
881
|
+
</form>
|
|
882
|
+
<ul className="divide-y divide-neutral-800 border-y border-neutral-800">
|
|
883
|
+
{todos.map((todo) => (
|
|
884
|
+
<li className="py-3" key={todo.id}>{todo.text}</li>
|
|
885
|
+
))}
|
|
886
|
+
</ul>
|
|
887
|
+
</section>
|
|
888
|
+
</main>
|
|
889
|
+
);
|
|
890
|
+
}
|
|
891
|
+
`,
|
|
892
|
+
"shared/todo.ts": `export type Todo = {
|
|
893
|
+
id: string;
|
|
894
|
+
text: string;
|
|
895
|
+
done: boolean;
|
|
896
|
+
ownerId: string;
|
|
897
|
+
createdAt: string;
|
|
898
|
+
updatedAt: string;
|
|
899
|
+
};
|
|
900
|
+
|
|
901
|
+
export function cleanTodoText(value: string): string {
|
|
902
|
+
return value.trim().slice(0, 160);
|
|
903
|
+
}
|
|
904
|
+
`,
|
|
905
|
+
"README.md": `# ${title}
|
|
906
|
+
|
|
907
|
+
Run this Lakebed capsule:
|
|
908
|
+
|
|
909
|
+
\`\`\`sh
|
|
910
|
+
pnpm lakebed dev ${name}
|
|
911
|
+
\`\`\`
|
|
912
|
+
`
|
|
913
|
+
};
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
async function newCommand(args) {
|
|
917
|
+
const [name] = positionals(args);
|
|
918
|
+
const template = readArg(args, "--template", "todo");
|
|
919
|
+
|
|
920
|
+
if (!name) {
|
|
921
|
+
usage();
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
if (template !== "todo") {
|
|
926
|
+
throw new Error(`Unknown template: ${template}`);
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
const targetDir = resolveCapsuleDir(name);
|
|
930
|
+
if (existsSync(targetDir)) {
|
|
931
|
+
throw new Error(`Target already exists: ${targetDir}`);
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
const files = todoTemplate(name);
|
|
935
|
+
for (const [path, contents] of Object.entries(files)) {
|
|
936
|
+
const absolutePath = join(targetDir, path);
|
|
937
|
+
await mkdir(dirname(absolutePath), { recursive: true });
|
|
938
|
+
await writeFile(absolutePath, contents);
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
console.log(`Created Lakebed capsule at ${targetDir}`);
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
async function runMany(args) {
|
|
945
|
+
const [capsuleArg] = positionals(args);
|
|
946
|
+
const capsuleDir = resolveCapsuleDir(capsuleArg);
|
|
947
|
+
const count = readNumberArg(args, "--count", 20);
|
|
948
|
+
const basePort = readNumberArg(args, "--base-port", 4000);
|
|
949
|
+
const handles = [];
|
|
950
|
+
|
|
951
|
+
try {
|
|
952
|
+
for (let index = 0; index < count; index += 1) {
|
|
953
|
+
handles.push(
|
|
954
|
+
await startDevServer({
|
|
955
|
+
capsuleDir,
|
|
956
|
+
port: basePort + index,
|
|
957
|
+
capsuleId: `run-many-${basePort + index}`,
|
|
958
|
+
quiet: true
|
|
959
|
+
})
|
|
960
|
+
);
|
|
961
|
+
}
|
|
962
|
+
} catch (error) {
|
|
963
|
+
await Promise.allSettled(handles.map((handle) => handle.close()));
|
|
964
|
+
throw error;
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
console.log(`Started ${handles.length} Lakebed capsules:`);
|
|
968
|
+
for (const handle of handles) {
|
|
969
|
+
console.log(handle.url);
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
async function stop() {
|
|
973
|
+
await Promise.allSettled(handles.map((handle) => handle.close()));
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
process.once("SIGINT", async () => {
|
|
977
|
+
await stop();
|
|
978
|
+
process.exit(0);
|
|
979
|
+
});
|
|
980
|
+
process.once("SIGTERM", async () => {
|
|
981
|
+
await stop();
|
|
982
|
+
process.exit(0);
|
|
983
|
+
});
|
|
984
|
+
|
|
985
|
+
await new Promise(() => {});
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
async function main() {
|
|
989
|
+
const [command, ...args] = process.argv.slice(2);
|
|
990
|
+
|
|
991
|
+
if (command === "new") {
|
|
992
|
+
await newCommand(args);
|
|
993
|
+
return;
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
if (command === "dev") {
|
|
997
|
+
await dev(args);
|
|
998
|
+
return;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
if (command === "build") {
|
|
1002
|
+
await buildCommand(args);
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
if (command === "deploy") {
|
|
1007
|
+
await deployCommand(args);
|
|
1008
|
+
return;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
if (command === "anonymous-server") {
|
|
1012
|
+
await anonymousServerCommand(args);
|
|
1013
|
+
return;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
if (command === "inspect") {
|
|
1017
|
+
await inspectCommand(args);
|
|
1018
|
+
return;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
if (command === "run-many") {
|
|
1022
|
+
await runMany(args);
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
if (command === "auth") {
|
|
1027
|
+
await authCommand(args);
|
|
1028
|
+
return;
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
if (command === "db") {
|
|
1032
|
+
await dbCommand(args);
|
|
1033
|
+
return;
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
if (command === "logs") {
|
|
1037
|
+
await logsCommand(args);
|
|
1038
|
+
return;
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
usage();
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
function isMainModule() {
|
|
1045
|
+
try {
|
|
1046
|
+
return realpathSync(fileURLToPath(import.meta.url)) === realpathSync(process.argv[1]);
|
|
1047
|
+
} catch {
|
|
1048
|
+
return false;
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
if (isMainModule()) {
|
|
1053
|
+
main().catch((error) => {
|
|
1054
|
+
if (error instanceof AnonymousCompilerError) {
|
|
1055
|
+
console.error("Anonymous build failed:");
|
|
1056
|
+
for (const diagnostic of error.diagnostics) {
|
|
1057
|
+
console.error(`- ${diagnostic.file}: ${diagnostic.message}`);
|
|
1058
|
+
}
|
|
1059
|
+
process.exitCode = 1;
|
|
1060
|
+
return;
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
console.error(error);
|
|
1064
|
+
process.exitCode = 1;
|
|
1065
|
+
});
|
|
1066
|
+
}
|