lopata 0.9.0 → 0.10.0
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/dist/dashboard/{chunk-yq8n0mcf.js → chunk-hnsny9g7.js} +230 -79
- package/dist/dashboard/{chunk-csyd2tq2.css → chunk-jzyhpjad.css} +8 -0
- package/dist/dashboard/index.html +1 -1
- package/package.json +1 -1
- package/src/api/handlers/routes.ts +5 -0
- package/src/api/index.ts +6 -2
- package/src/api/types.ts +7 -0
- package/src/cli/dev.ts +39 -2
- package/src/lopata-config.ts +1 -0
- package/src/route-matcher.ts +11 -0
- package/src/vite-plugin/dev-server-plugin.ts +83 -40
- package/src/vite-plugin/index.ts +1 -1
- package/src/vite-plugin/modules-plugin.ts +3 -0
|
@@ -1142,6 +1142,50 @@ function toError(err) {
|
|
|
1142
1142
|
return err instanceof Error ? err : new Error(String(err));
|
|
1143
1143
|
}
|
|
1144
1144
|
|
|
1145
|
+
// src/dashboard/components/copy-markdown-button.tsx
|
|
1146
|
+
function CopyMarkdownButton({ getMarkdown, title }) {
|
|
1147
|
+
const [copied, setCopied] = d2(false);
|
|
1148
|
+
const timerRef = A2(null);
|
|
1149
|
+
y2(() => {
|
|
1150
|
+
return () => {
|
|
1151
|
+
if (timerRef.current)
|
|
1152
|
+
clearTimeout(timerRef.current);
|
|
1153
|
+
};
|
|
1154
|
+
}, []);
|
|
1155
|
+
const handleCopy = (e3) => {
|
|
1156
|
+
e3.stopPropagation();
|
|
1157
|
+
navigator.clipboard.writeText(getMarkdown()).then(() => {
|
|
1158
|
+
setCopied(true);
|
|
1159
|
+
if (timerRef.current)
|
|
1160
|
+
clearTimeout(timerRef.current);
|
|
1161
|
+
timerRef.current = setTimeout(() => setCopied(false), 1500);
|
|
1162
|
+
});
|
|
1163
|
+
};
|
|
1164
|
+
return /* @__PURE__ */ u3("button", {
|
|
1165
|
+
onClick: handleCopy,
|
|
1166
|
+
class: "rounded-md px-2 py-1 text-xs font-medium text-text-muted hover:text-text-data hover:bg-panel-hover border border-transparent hover:border-border transition-all",
|
|
1167
|
+
title: title ?? "Copy as Markdown",
|
|
1168
|
+
children: copied ? "Copied!" : "Copy MD"
|
|
1169
|
+
}, undefined, false, undefined, this);
|
|
1170
|
+
}
|
|
1171
|
+
function tableToMarkdown(headers, rows) {
|
|
1172
|
+
const escape = (v3) => String(v3 ?? "").replace(/\|/g, "\\|").replace(/\n/g, " ");
|
|
1173
|
+
const headerRow = `| ${headers.map(escape).join(" | ")} |`;
|
|
1174
|
+
const separator = `| ${headers.map(() => "---").join(" | ")} |`;
|
|
1175
|
+
const dataRows = rows.map((row) => `| ${row.map(escape).join(" | ")} |`);
|
|
1176
|
+
return [headerRow, separator, ...dataRows].join(`
|
|
1177
|
+
`);
|
|
1178
|
+
}
|
|
1179
|
+
function recordsToMarkdown(headers, rows) {
|
|
1180
|
+
return tableToMarkdown(headers, rows.map((row) => headers.map((h3) => row[h3])));
|
|
1181
|
+
}
|
|
1182
|
+
function keyValueToMarkdown(data) {
|
|
1183
|
+
const entries = Object.entries(data);
|
|
1184
|
+
if (entries.length === 0)
|
|
1185
|
+
return "";
|
|
1186
|
+
return tableToMarkdown(["Key", "Value"], entries);
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1145
1189
|
// src/dashboard/sql-browser/editable-cell.tsx
|
|
1146
1190
|
function EditableCell({ value, onSave, foreignKey, onNavigateFK, onInspect, alignRight }) {
|
|
1147
1191
|
const [editing, setEditing] = d2(false);
|
|
@@ -2354,6 +2398,9 @@ function TableDataView({ table, execQuery, onOpenInConsole, history: history2, b
|
|
|
2354
2398
|
}, undefined, true, undefined, this)
|
|
2355
2399
|
]
|
|
2356
2400
|
}, undefined, true, undefined, this),
|
|
2401
|
+
/* @__PURE__ */ u3(CopyMarkdownButton, {
|
|
2402
|
+
getMarkdown: () => recordsToMarkdown(displayCols, rows)
|
|
2403
|
+
}, undefined, false, undefined, this),
|
|
2357
2404
|
/* @__PURE__ */ u3("button", {
|
|
2358
2405
|
onClick: () => setShowInsert(!showInsert),
|
|
2359
2406
|
class: `rounded-md px-3 py-1.5 text-sm font-medium transition-all ${showInsert ? "bg-panel-active text-text-data" : "bg-ink text-surface hover:opacity-80"}`,
|
|
@@ -3017,33 +3064,41 @@ function SqlConsoleTab({ execQuery, generateSql, initialSql, history: history2 }
|
|
|
3017
3064
|
function ResultTable({ columns, rows }) {
|
|
3018
3065
|
return /* @__PURE__ */ u3("div", {
|
|
3019
3066
|
class: "bg-panel rounded-lg border border-border overflow-x-auto",
|
|
3020
|
-
children:
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
/* @__PURE__ */ u3(
|
|
3024
|
-
|
|
3025
|
-
class: "border-b border-border-subtle",
|
|
3026
|
-
children: columns.map((col) => /* @__PURE__ */ u3("th", {
|
|
3027
|
-
class: "text-left px-4 py-2.5 font-medium text-xs text-text-muted uppercase tracking-wider font-mono",
|
|
3028
|
-
children: col
|
|
3029
|
-
}, col, false, undefined, this))
|
|
3030
|
-
}, undefined, false, undefined, this)
|
|
3031
|
-
}, undefined, false, undefined, this),
|
|
3032
|
-
/* @__PURE__ */ u3("tbody", {
|
|
3033
|
-
children: rows.map((row, i3) => /* @__PURE__ */ u3("tr", {
|
|
3034
|
-
class: "group border-b border-border-row last:border-0 hover:bg-panel-hover/50 transition-colors",
|
|
3035
|
-
children: columns.map((col) => /* @__PURE__ */ u3("td", {
|
|
3036
|
-
class: "px-4 py-2.5 font-mono text-xs",
|
|
3037
|
-
children: row[col] === null ? /* @__PURE__ */ u3("span", {
|
|
3038
|
-
class: "text-text-dim italic",
|
|
3039
|
-
children: "NULL"
|
|
3040
|
-
}, undefined, false, undefined, this) : String(row[col])
|
|
3041
|
-
}, col, false, undefined, this))
|
|
3042
|
-
}, i3, false, undefined, this))
|
|
3067
|
+
children: [
|
|
3068
|
+
/* @__PURE__ */ u3("div", {
|
|
3069
|
+
class: "flex justify-end px-2 pt-2",
|
|
3070
|
+
children: /* @__PURE__ */ u3(CopyMarkdownButton, {
|
|
3071
|
+
getMarkdown: () => recordsToMarkdown(columns, rows)
|
|
3043
3072
|
}, undefined, false, undefined, this)
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3073
|
+
}, undefined, false, undefined, this),
|
|
3074
|
+
/* @__PURE__ */ u3("table", {
|
|
3075
|
+
class: "w-full text-sm",
|
|
3076
|
+
children: [
|
|
3077
|
+
/* @__PURE__ */ u3("thead", {
|
|
3078
|
+
children: /* @__PURE__ */ u3("tr", {
|
|
3079
|
+
class: "border-b border-border-subtle",
|
|
3080
|
+
children: columns.map((col) => /* @__PURE__ */ u3("th", {
|
|
3081
|
+
class: "text-left px-4 py-2.5 font-medium text-xs text-text-muted uppercase tracking-wider font-mono",
|
|
3082
|
+
children: col
|
|
3083
|
+
}, col, false, undefined, this))
|
|
3084
|
+
}, undefined, false, undefined, this)
|
|
3085
|
+
}, undefined, false, undefined, this),
|
|
3086
|
+
/* @__PURE__ */ u3("tbody", {
|
|
3087
|
+
children: rows.map((row, i3) => /* @__PURE__ */ u3("tr", {
|
|
3088
|
+
class: "group border-b border-border-row last:border-0 hover:bg-panel-hover/50 transition-colors",
|
|
3089
|
+
children: columns.map((col) => /* @__PURE__ */ u3("td", {
|
|
3090
|
+
class: "px-4 py-2.5 font-mono text-xs",
|
|
3091
|
+
children: row[col] === null ? /* @__PURE__ */ u3("span", {
|
|
3092
|
+
class: "text-text-dim italic",
|
|
3093
|
+
children: "NULL"
|
|
3094
|
+
}, undefined, false, undefined, this) : String(row[col])
|
|
3095
|
+
}, col, false, undefined, this))
|
|
3096
|
+
}, i3, false, undefined, this))
|
|
3097
|
+
}, undefined, false, undefined, this)
|
|
3098
|
+
]
|
|
3099
|
+
}, undefined, true, undefined, this)
|
|
3100
|
+
]
|
|
3101
|
+
}, undefined, true, undefined, this);
|
|
3047
3102
|
}
|
|
3048
3103
|
|
|
3049
3104
|
// src/dashboard/sql-browser/sql-browser.tsx
|
|
@@ -3195,25 +3250,35 @@ function KeyValueTable({ data }) {
|
|
|
3195
3250
|
children: "No entries"
|
|
3196
3251
|
}, undefined, false, undefined, this);
|
|
3197
3252
|
}
|
|
3198
|
-
return /* @__PURE__ */ u3("
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
/* @__PURE__ */ u3("
|
|
3210
|
-
class: "
|
|
3211
|
-
children:
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3253
|
+
return /* @__PURE__ */ u3("div", {
|
|
3254
|
+
children: [
|
|
3255
|
+
/* @__PURE__ */ u3("div", {
|
|
3256
|
+
class: "flex justify-end px-2 pt-1",
|
|
3257
|
+
children: /* @__PURE__ */ u3(CopyMarkdownButton, {
|
|
3258
|
+
getMarkdown: () => keyValueToMarkdown(data)
|
|
3259
|
+
}, undefined, false, undefined, this)
|
|
3260
|
+
}, undefined, false, undefined, this),
|
|
3261
|
+
/* @__PURE__ */ u3("table", {
|
|
3262
|
+
class: "w-full text-sm",
|
|
3263
|
+
children: /* @__PURE__ */ u3("tbody", {
|
|
3264
|
+
children: entries.map(([key, value]) => /* @__PURE__ */ u3("tr", {
|
|
3265
|
+
class: "border-b border-border-subtle last:border-0 hover:bg-panel-hover/50 transition-colors",
|
|
3266
|
+
children: [
|
|
3267
|
+
/* @__PURE__ */ u3("td", {
|
|
3268
|
+
class: "px-4 py-2 font-medium text-text-secondary whitespace-nowrap align-top font-mono",
|
|
3269
|
+
style: "width: 1%;",
|
|
3270
|
+
children: key
|
|
3271
|
+
}, undefined, false, undefined, this),
|
|
3272
|
+
/* @__PURE__ */ u3("td", {
|
|
3273
|
+
class: "px-4 py-2 text-ink break-all font-mono",
|
|
3274
|
+
children: value
|
|
3275
|
+
}, undefined, false, undefined, this)
|
|
3276
|
+
]
|
|
3277
|
+
}, key, true, undefined, this))
|
|
3278
|
+
}, undefined, false, undefined, this)
|
|
3279
|
+
}, undefined, false, undefined, this)
|
|
3280
|
+
]
|
|
3281
|
+
}, undefined, true, undefined, this);
|
|
3217
3282
|
}
|
|
3218
3283
|
// src/dashboard/components/page-header.tsx
|
|
3219
3284
|
function PageHeader({ title, subtitle, actions }) {
|
|
@@ -3341,30 +3406,38 @@ function StatusBadge({ status, colorMap }) {
|
|
|
3341
3406
|
function Table({ headers, rows }) {
|
|
3342
3407
|
return /* @__PURE__ */ u3("div", {
|
|
3343
3408
|
class: "bg-panel rounded-lg border border-border overflow-x-auto",
|
|
3344
|
-
children:
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
/* @__PURE__ */ u3(
|
|
3348
|
-
|
|
3349
|
-
class: "border-b border-border-subtle",
|
|
3350
|
-
children: headers.map((h3) => /* @__PURE__ */ u3("th", {
|
|
3351
|
-
class: "text-left px-4 py-3 font-mono font-medium text-xs text-text-muted uppercase tracking-wider",
|
|
3352
|
-
children: h3
|
|
3353
|
-
}, h3, false, undefined, this))
|
|
3354
|
-
}, undefined, false, undefined, this)
|
|
3355
|
-
}, undefined, false, undefined, this),
|
|
3356
|
-
/* @__PURE__ */ u3("tbody", {
|
|
3357
|
-
children: rows.map((row, i3) => /* @__PURE__ */ u3("tr", {
|
|
3358
|
-
class: "group border-b border-border-row last:border-0 hover:bg-panel-hover/50 transition-colors",
|
|
3359
|
-
children: row.map((cell, j3) => /* @__PURE__ */ u3("td", {
|
|
3360
|
-
class: "px-4 py-3",
|
|
3361
|
-
children: cell
|
|
3362
|
-
}, j3, false, undefined, this))
|
|
3363
|
-
}, i3, false, undefined, this))
|
|
3409
|
+
children: [
|
|
3410
|
+
/* @__PURE__ */ u3("div", {
|
|
3411
|
+
class: "flex justify-end px-2 pt-2",
|
|
3412
|
+
children: /* @__PURE__ */ u3(CopyMarkdownButton, {
|
|
3413
|
+
getMarkdown: () => tableToMarkdown(headers, rows)
|
|
3364
3414
|
}, undefined, false, undefined, this)
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3415
|
+
}, undefined, false, undefined, this),
|
|
3416
|
+
/* @__PURE__ */ u3("table", {
|
|
3417
|
+
class: "w-full text-sm",
|
|
3418
|
+
children: [
|
|
3419
|
+
/* @__PURE__ */ u3("thead", {
|
|
3420
|
+
children: /* @__PURE__ */ u3("tr", {
|
|
3421
|
+
class: "border-b border-border-subtle",
|
|
3422
|
+
children: headers.map((h3) => /* @__PURE__ */ u3("th", {
|
|
3423
|
+
class: "text-left px-4 py-3 font-mono font-medium text-xs text-text-muted uppercase tracking-wider",
|
|
3424
|
+
children: h3
|
|
3425
|
+
}, h3, false, undefined, this))
|
|
3426
|
+
}, undefined, false, undefined, this)
|
|
3427
|
+
}, undefined, false, undefined, this),
|
|
3428
|
+
/* @__PURE__ */ u3("tbody", {
|
|
3429
|
+
children: rows.map((row, i3) => /* @__PURE__ */ u3("tr", {
|
|
3430
|
+
class: "group border-b border-border-row last:border-0 hover:bg-panel-hover/50 transition-colors",
|
|
3431
|
+
children: row.map((cell, j3) => /* @__PURE__ */ u3("td", {
|
|
3432
|
+
class: "px-4 py-3",
|
|
3433
|
+
children: cell
|
|
3434
|
+
}, j3, false, undefined, this))
|
|
3435
|
+
}, i3, false, undefined, this))
|
|
3436
|
+
}, undefined, false, undefined, this)
|
|
3437
|
+
]
|
|
3438
|
+
}, undefined, true, undefined, this)
|
|
3439
|
+
]
|
|
3440
|
+
}, undefined, true, undefined, this);
|
|
3368
3441
|
}
|
|
3369
3442
|
// src/dashboard/components/table-link.tsx
|
|
3370
3443
|
function TableLink({ href, children, mono }) {
|
|
@@ -5675,11 +5748,25 @@ function ErrorList() {
|
|
|
5675
5748
|
/* @__PURE__ */ u3(RefreshButton, {
|
|
5676
5749
|
onClick: () => loadErrors()
|
|
5677
5750
|
}, undefined, false, undefined, this),
|
|
5678
|
-
errors.length > 0 && /* @__PURE__ */ u3(
|
|
5679
|
-
|
|
5680
|
-
|
|
5681
|
-
|
|
5682
|
-
|
|
5751
|
+
errors.length > 0 && /* @__PURE__ */ u3(k, {
|
|
5752
|
+
children: [
|
|
5753
|
+
/* @__PURE__ */ u3(CopyMarkdownButton, {
|
|
5754
|
+
getMarkdown: () => tableToMarkdown(["Source", "Error", "Message", "Context", "Worker", "Time"], errors.map((err) => [
|
|
5755
|
+
err.source ?? "-",
|
|
5756
|
+
err.errorName,
|
|
5757
|
+
err.errorMessage,
|
|
5758
|
+
err.requestMethod && err.requestUrl ? `${err.requestMethod} ${err.requestUrl}` : "-",
|
|
5759
|
+
err.workerName ?? "-",
|
|
5760
|
+
formatTimestamp2(err.timestamp)
|
|
5761
|
+
]))
|
|
5762
|
+
}, undefined, false, undefined, this),
|
|
5763
|
+
/* @__PURE__ */ u3("button", {
|
|
5764
|
+
onClick: handleClear,
|
|
5765
|
+
class: "rounded-md px-3 py-1.5 text-sm font-medium bg-panel border border-border text-text-secondary btn-danger transition-all",
|
|
5766
|
+
children: "Clear all"
|
|
5767
|
+
}, undefined, false, undefined, this)
|
|
5768
|
+
]
|
|
5769
|
+
}, undefined, true, undefined, this)
|
|
5683
5770
|
]
|
|
5684
5771
|
}, undefined, true, undefined, this)
|
|
5685
5772
|
]
|
|
@@ -5889,11 +5976,20 @@ function ErrorDetailPage({ errorId }) {
|
|
|
5889
5976
|
}, undefined, true, undefined, this)
|
|
5890
5977
|
]
|
|
5891
5978
|
}, undefined, true, undefined, this),
|
|
5892
|
-
/* @__PURE__ */ u3("
|
|
5893
|
-
|
|
5894
|
-
|
|
5895
|
-
|
|
5896
|
-
|
|
5979
|
+
/* @__PURE__ */ u3("div", {
|
|
5980
|
+
class: "flex items-center gap-2",
|
|
5981
|
+
children: [
|
|
5982
|
+
/* @__PURE__ */ u3(CopyMarkdownButton, {
|
|
5983
|
+
getMarkdown: () => errorToMarkdown(detail),
|
|
5984
|
+
title: "Copy error as Markdown for LLM"
|
|
5985
|
+
}, undefined, false, undefined, this),
|
|
5986
|
+
/* @__PURE__ */ u3("button", {
|
|
5987
|
+
onClick: handleDelete,
|
|
5988
|
+
class: "rounded-md px-3 py-1.5 text-sm font-medium bg-panel border border-border text-text-secondary btn-danger transition-all",
|
|
5989
|
+
children: "Delete"
|
|
5990
|
+
}, undefined, false, undefined, this)
|
|
5991
|
+
]
|
|
5992
|
+
}, undefined, true, undefined, this)
|
|
5897
5993
|
]
|
|
5898
5994
|
}, undefined, true, undefined, this),
|
|
5899
5995
|
/* @__PURE__ */ u3("div", {
|
|
@@ -6296,6 +6392,60 @@ function truncateUrl(url) {
|
|
|
6296
6392
|
return url.length > 50 ? url.slice(0, 50) + "..." : url;
|
|
6297
6393
|
}
|
|
6298
6394
|
}
|
|
6395
|
+
function errorToMarkdown(detail) {
|
|
6396
|
+
const { data } = detail;
|
|
6397
|
+
const lines = [];
|
|
6398
|
+
lines.push(`## ${data.error.name}: ${data.error.message}`);
|
|
6399
|
+
if (detail.source)
|
|
6400
|
+
lines.push(`**Source:** ${detail.source}`);
|
|
6401
|
+
if (data.runtime.workerName)
|
|
6402
|
+
lines.push(`**Worker:** ${data.runtime.workerName}`);
|
|
6403
|
+
lines.push("");
|
|
6404
|
+
if (data.error.stack) {
|
|
6405
|
+
lines.push("### Stack Trace");
|
|
6406
|
+
lines.push("```");
|
|
6407
|
+
lines.push(data.error.stack);
|
|
6408
|
+
lines.push("```");
|
|
6409
|
+
lines.push("");
|
|
6410
|
+
}
|
|
6411
|
+
if (data.error.frames.length > 0) {
|
|
6412
|
+
lines.push("### Source Code");
|
|
6413
|
+
for (const frame of data.error.frames) {
|
|
6414
|
+
lines.push(`#### ${frame.file}:${frame.line}:${frame.column}${frame.function ? ` in ${frame.function}` : ""}`);
|
|
6415
|
+
if (frame.source && frame.source.length > 0) {
|
|
6416
|
+
const startLine = frame.line - (frame.sourceLine ?? 0);
|
|
6417
|
+
lines.push("```");
|
|
6418
|
+
frame.source.forEach((line, i3) => {
|
|
6419
|
+
const lineNum = startLine + i3;
|
|
6420
|
+
const marker = i3 === frame.sourceLine ? ">" : " ";
|
|
6421
|
+
lines.push(`${marker} ${lineNum} | ${line}`);
|
|
6422
|
+
});
|
|
6423
|
+
lines.push("```");
|
|
6424
|
+
}
|
|
6425
|
+
lines.push("");
|
|
6426
|
+
}
|
|
6427
|
+
}
|
|
6428
|
+
if (data.request.method && data.request.url) {
|
|
6429
|
+
lines.push("### Request");
|
|
6430
|
+
lines.push(`${data.request.method} ${data.request.url}`);
|
|
6431
|
+
const headers = Object.entries(data.request.headers);
|
|
6432
|
+
if (headers.length > 0) {
|
|
6433
|
+
lines.push("");
|
|
6434
|
+
lines.push(keyValueToMarkdown(data.request.headers));
|
|
6435
|
+
}
|
|
6436
|
+
lines.push("");
|
|
6437
|
+
}
|
|
6438
|
+
if (data.bindings.length > 0) {
|
|
6439
|
+
lines.push("### Bindings");
|
|
6440
|
+
lines.push(tableToMarkdown(["Name", "Type"], data.bindings.map((b) => [b.name, b.type])));
|
|
6441
|
+
lines.push("");
|
|
6442
|
+
}
|
|
6443
|
+
lines.push("### Runtime");
|
|
6444
|
+
lines.push(`- **Bun:** ${data.runtime.bunVersion}`);
|
|
6445
|
+
lines.push(`- **Platform:** ${data.runtime.platform} / ${data.runtime.arch}`);
|
|
6446
|
+
return lines.join(`
|
|
6447
|
+
`);
|
|
6448
|
+
}
|
|
6299
6449
|
|
|
6300
6450
|
// src/dashboard/views/generations.tsx
|
|
6301
6451
|
var STATE_COLORS = {
|
|
@@ -7861,6 +8011,7 @@ function R2ObjectList({ bucket }) {
|
|
|
7861
8011
|
// src/dashboard/views/routes.tsx
|
|
7862
8012
|
var TYPE_COLORS = {
|
|
7863
8013
|
route: "bg-emerald-500/15 text-emerald-500",
|
|
8014
|
+
host: "bg-blue-500/15 text-blue-500",
|
|
7864
8015
|
fallback: "bg-panel-active text-text-data"
|
|
7865
8016
|
};
|
|
7866
8017
|
function RoutesView() {
|
|
@@ -7886,7 +8037,7 @@ function RoutesView() {
|
|
|
7886
8037
|
children: r3.workerName
|
|
7887
8038
|
}, undefined, false, undefined, this),
|
|
7888
8039
|
/* @__PURE__ */ u3(StatusBadge, {
|
|
7889
|
-
status: r3.isFallback ? "fallback" : "route",
|
|
8040
|
+
status: r3.isFallback ? "fallback" : r3.type === "host" ? "host" : "route",
|
|
7890
8041
|
colorMap: TYPE_COLORS
|
|
7891
8042
|
}, undefined, false, undefined, this)
|
|
7892
8043
|
])
|
|
@@ -1756,6 +1756,14 @@
|
|
|
1756
1756
|
padding-top: calc(var(--spacing) * .5);
|
|
1757
1757
|
}
|
|
1758
1758
|
|
|
1759
|
+
.pt-1 {
|
|
1760
|
+
padding-top: calc(var(--spacing) * 1);
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
.pt-2 {
|
|
1764
|
+
padding-top: calc(var(--spacing) * 2);
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1759
1767
|
.pt-3 {
|
|
1760
1768
|
padding-top: calc(var(--spacing) * 3);
|
|
1761
1769
|
}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
9
9
|
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet" />
|
|
10
10
|
|
|
11
|
-
<link rel="stylesheet" crossorigin href="/__dashboard/assets/chunk-
|
|
11
|
+
<link rel="stylesheet" crossorigin href="/__dashboard/assets/chunk-jzyhpjad.css"><script type="module" crossorigin src="/__dashboard/assets/chunk-hnsny9g7.js"></script></head>
|
|
12
12
|
<body class="h-full bg-surface text-ink" style="font-family: system-ui, -apple-system, sans-serif;">
|
|
13
13
|
<script>
|
|
14
14
|
// Apply saved theme before first paint to prevent flash
|
package/package.json
CHANGED
|
@@ -5,6 +5,11 @@ export const handlers = {
|
|
|
5
5
|
'routes.list'(_input: {}, ctx: HandlerContext): RouteInfo[] {
|
|
6
6
|
const routes: RouteInfo[] = []
|
|
7
7
|
|
|
8
|
+
// Show host-based routes
|
|
9
|
+
for (const hr of ctx.hostRoutes) {
|
|
10
|
+
routes.push({ pattern: hr.pattern, workerName: hr.workerName, isFallback: false, type: 'host' })
|
|
11
|
+
}
|
|
12
|
+
|
|
8
13
|
if (ctx.routeDispatcher) {
|
|
9
14
|
for (const r of ctx.routeDispatcher.getRegisteredRoutes()) {
|
|
10
15
|
routes.push({ pattern: r.pattern, workerName: r.workerName, isFallback: false })
|
package/src/api/index.ts
CHANGED
|
@@ -6,9 +6,9 @@ import type { WorkerRegistry } from '../worker-registry'
|
|
|
6
6
|
import { handlePreflight, withCors } from './cors'
|
|
7
7
|
import { dispatch } from './dispatch'
|
|
8
8
|
import { handleR2Download, handleR2Upload } from './r2'
|
|
9
|
-
import type { HandlerContext } from './types'
|
|
9
|
+
import type { HandlerContext, HostRouteInfo } from './types'
|
|
10
10
|
|
|
11
|
-
const ctx: HandlerContext = { config: null, manager: null, registry: null, lopataConfig: null, routeDispatcher: null }
|
|
11
|
+
const ctx: HandlerContext = { config: null, manager: null, registry: null, lopataConfig: null, routeDispatcher: null, hostRoutes: [] }
|
|
12
12
|
|
|
13
13
|
export function setDashboardConfig(config: WranglerConfig): void {
|
|
14
14
|
ctx.config = config
|
|
@@ -30,6 +30,10 @@ export function setRouteDispatcher(dispatcher: RouteDispatcher): void {
|
|
|
30
30
|
ctx.routeDispatcher = dispatcher
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
export function setHostRoutes(routes: HostRouteInfo[]): void {
|
|
34
|
+
ctx.hostRoutes = routes
|
|
35
|
+
}
|
|
36
|
+
|
|
33
37
|
export function handleApiRequest(request: Request): Response | Promise<Response> {
|
|
34
38
|
const url = new URL(request.url)
|
|
35
39
|
|
package/src/api/types.ts
CHANGED
|
@@ -382,18 +382,25 @@ import type { LopataConfig } from '../lopata-config'
|
|
|
382
382
|
import type { RouteDispatcher } from '../route-matcher'
|
|
383
383
|
import type { WorkerRegistry } from '../worker-registry'
|
|
384
384
|
|
|
385
|
+
export interface HostRouteInfo {
|
|
386
|
+
pattern: string
|
|
387
|
+
workerName: string
|
|
388
|
+
}
|
|
389
|
+
|
|
385
390
|
export interface HandlerContext {
|
|
386
391
|
config: WranglerConfig | null
|
|
387
392
|
manager: GenerationManager | null
|
|
388
393
|
registry: WorkerRegistry | null
|
|
389
394
|
lopataConfig: LopataConfig | null
|
|
390
395
|
routeDispatcher: RouteDispatcher | null
|
|
396
|
+
hostRoutes: HostRouteInfo[]
|
|
391
397
|
}
|
|
392
398
|
|
|
393
399
|
export interface RouteInfo {
|
|
394
400
|
pattern: string
|
|
395
401
|
workerName: string
|
|
396
402
|
isFallback: boolean
|
|
403
|
+
type?: 'path' | 'host'
|
|
397
404
|
}
|
|
398
405
|
|
|
399
406
|
/** Collect configs from all workers (registry) or fall back to single config. */
|
package/src/cli/dev.ts
CHANGED
|
@@ -3,7 +3,15 @@ Error.stackTraceLimit = 50
|
|
|
3
3
|
|
|
4
4
|
import '../plugin'
|
|
5
5
|
import path from 'node:path'
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
handleApiRequest,
|
|
8
|
+
setDashboardConfig,
|
|
9
|
+
setGenerationManager,
|
|
10
|
+
setHostRoutes,
|
|
11
|
+
setLopataConfig,
|
|
12
|
+
setRouteDispatcher,
|
|
13
|
+
setWorkerRegistry,
|
|
14
|
+
} from '../api'
|
|
7
15
|
import { QueuePullConsumer } from '../bindings/queue'
|
|
8
16
|
import type { AckRequest, PullRequest } from '../bindings/queue'
|
|
9
17
|
import { CFWebSocket } from '../bindings/websocket-pair'
|
|
@@ -14,7 +22,7 @@ import { FileWatcher } from '../file-watcher'
|
|
|
14
22
|
import { GenerationManager } from '../generation-manager'
|
|
15
23
|
import { loadLopataConfig } from '../lopata-config'
|
|
16
24
|
import { addCfProperty } from '../request-cf'
|
|
17
|
-
import { RouteDispatcher } from '../route-matcher'
|
|
25
|
+
import { matchHost, RouteDispatcher } from '../route-matcher'
|
|
18
26
|
import { getTraceStore } from '../tracing/store'
|
|
19
27
|
import type { TraceEvent } from '../tracing/types'
|
|
20
28
|
import { WorkerRegistry } from '../worker-registry'
|
|
@@ -35,6 +43,7 @@ export async function run(ctx: CliContext) {
|
|
|
35
43
|
let manager: GenerationManager
|
|
36
44
|
let routeDispatcher: RouteDispatcher | undefined
|
|
37
45
|
let registry: WorkerRegistry | undefined
|
|
46
|
+
const hostRoutes: Array<{ pattern: string; manager: GenerationManager; workerName: string }> = []
|
|
38
47
|
|
|
39
48
|
if (lopataConfig) {
|
|
40
49
|
// ─── Multi-worker mode ─────────────────────────────────────────
|
|
@@ -143,6 +152,21 @@ export async function run(ctx: CliContext) {
|
|
|
143
152
|
}
|
|
144
153
|
}
|
|
145
154
|
|
|
155
|
+
// Build host-based routing map
|
|
156
|
+
for (const workerDef of lopataConfig.workers ?? []) {
|
|
157
|
+
if (!workerDef.hosts) continue
|
|
158
|
+
const auxMgr = registry.getManager(workerDef.name)
|
|
159
|
+
if (!auxMgr) continue
|
|
160
|
+
for (const host of workerDef.hosts) {
|
|
161
|
+
hostRoutes.push({ pattern: host, manager: auxMgr, workerName: workerDef.name })
|
|
162
|
+
console.log(`[lopata] Host route: ${host} → ${workerDef.name}`)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (hostRoutes.length > 0) {
|
|
167
|
+
setHostRoutes(hostRoutes.map(hr => ({ pattern: hr.pattern, workerName: hr.workerName })))
|
|
168
|
+
}
|
|
169
|
+
|
|
146
170
|
manager = mainManager
|
|
147
171
|
setGenerationManager(manager)
|
|
148
172
|
setWorkerRegistry(registry)
|
|
@@ -256,6 +280,19 @@ export async function run(ctx: CliContext) {
|
|
|
256
280
|
return gen.callScheduled(cronExpr)
|
|
257
281
|
}
|
|
258
282
|
|
|
283
|
+
// Host-based dispatch: match Host header against configured patterns
|
|
284
|
+
if (hostRoutes.length > 0) {
|
|
285
|
+
const hostHeader = request.headers.get('host') ?? ''
|
|
286
|
+
const hostname = hostHeader.split(':')[0] ?? ''
|
|
287
|
+
for (const hr of hostRoutes) {
|
|
288
|
+
if (matchHost(hostname, hr.pattern)) {
|
|
289
|
+
const gen = hr.manager.active
|
|
290
|
+
if (!gen) return new Response('No active generation', { status: 503 })
|
|
291
|
+
return (await gen.callFetch(request, server)) as Response
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
259
296
|
// Delegate to active generation (route-based dispatch in multi-worker mode)
|
|
260
297
|
const targetManager = routeDispatcher ? routeDispatcher.resolve(url.pathname) : manager
|
|
261
298
|
const gen = targetManager.active
|
package/src/lopata-config.ts
CHANGED
package/src/route-matcher.ts
CHANGED
|
@@ -61,6 +61,17 @@ export function matchRoute(pathname: string, pattern: string): boolean {
|
|
|
61
61
|
return pathname === pattern
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
/** Match a hostname against a host pattern. Supports exact match and `*.domain` wildcards. */
|
|
65
|
+
export function matchHost(hostname: string, pattern: string): boolean {
|
|
66
|
+
if (pattern === hostname) return true
|
|
67
|
+
if (pattern.startsWith('*.')) {
|
|
68
|
+
const suffix = pattern.slice(1) // ".localhost"
|
|
69
|
+
// Must have a subdomain — bare hostname doesn't match *.localhost
|
|
70
|
+
return hostname.endsWith(suffix) && hostname.length > suffix.length
|
|
71
|
+
}
|
|
72
|
+
return false
|
|
73
|
+
}
|
|
74
|
+
|
|
64
75
|
/** Count the number of path segments in a pattern (ignoring trailing wildcard). */
|
|
65
76
|
function segmentCount(pattern: string): number {
|
|
66
77
|
const clean = pattern.replace(/\/?\*$/, '')
|
|
@@ -2,12 +2,13 @@ import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
|
2
2
|
import { dirname, resolve } from 'node:path'
|
|
3
3
|
import type { Plugin, ViteDevServer } from 'vite'
|
|
4
4
|
import { FileWatcher } from '../file-watcher.ts'
|
|
5
|
-
import {
|
|
5
|
+
import type { RoutableManager } from '../route-matcher.ts'
|
|
6
|
+
import { matchHost, RouteDispatcher } from '../route-matcher.ts'
|
|
6
7
|
|
|
7
8
|
interface DevServerPluginOptions {
|
|
8
9
|
configPath?: string
|
|
9
10
|
envName: string
|
|
10
|
-
auxiliaryWorkers?: { configPath: string; name?: string }[]
|
|
11
|
+
auxiliaryWorkers?: { configPath: string; name?: string; hosts?: string[] }[]
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
/**
|
|
@@ -52,6 +53,8 @@ export function devServerPlugin(options: DevServerPluginOptions): Plugin {
|
|
|
52
53
|
|
|
53
54
|
// Route dispatcher for multi-worker route-based dispatching
|
|
54
55
|
let routeDispatcher: RouteDispatcher | undefined
|
|
56
|
+
// Host-based routing: map host patterns to managers
|
|
57
|
+
const hostRoutes: Array<{ pattern: string; manager: RoutableManager; workerName: string }> = []
|
|
55
58
|
|
|
56
59
|
// Track current module to detect when Vite HMR invalidates it
|
|
57
60
|
let currentModule: Record<string, unknown> | null = null
|
|
@@ -203,6 +206,33 @@ export function devServerPlugin(options: DevServerPluginOptions): Plugin {
|
|
|
203
206
|
}
|
|
204
207
|
}
|
|
205
208
|
|
|
209
|
+
/**
|
|
210
|
+
* Resolve an auxiliary worker for the given request.
|
|
211
|
+
* Checks host-based routes first, then path-based route dispatcher.
|
|
212
|
+
* Returns null if the request should be handled by the main worker.
|
|
213
|
+
*/
|
|
214
|
+
function resolveAuxWorker(req: IncomingMessage, url: string): { manager: RoutableManager; workerName: string } | null {
|
|
215
|
+
// Host-based dispatch
|
|
216
|
+
if (hostRoutes.length > 0) {
|
|
217
|
+
const hostHeader = req.headers.host ?? ''
|
|
218
|
+
const hostname = hostHeader.split(':')[0] ?? ''
|
|
219
|
+
for (const hr of hostRoutes) {
|
|
220
|
+
if (matchHost(hostname, hr.pattern)) {
|
|
221
|
+
return hr
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
// Path-based dispatch
|
|
226
|
+
if (routeDispatcher) {
|
|
227
|
+
const parsedUrl = new URL(url, 'http://localhost')
|
|
228
|
+
const targetManager = routeDispatcher.resolve(parsedUrl.pathname)
|
|
229
|
+
if (!routeDispatcher.isFallback(targetManager)) {
|
|
230
|
+
return { manager: targetManager, workerName: (targetManager as any).config?.name ?? 'aux' }
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return null
|
|
234
|
+
}
|
|
235
|
+
|
|
206
236
|
return {
|
|
207
237
|
name: 'lopata:dev-server',
|
|
208
238
|
|
|
@@ -421,6 +451,22 @@ export function devServerPlugin(options: DevServerPluginOptions): Plugin {
|
|
|
421
451
|
}
|
|
422
452
|
}
|
|
423
453
|
apiMod.setRouteDispatcher(routeDispatcher)
|
|
454
|
+
|
|
455
|
+
// Build host-based routing map
|
|
456
|
+
for (const workerDef of options.auxiliaryWorkers) {
|
|
457
|
+
if (!workerDef.hosts) continue
|
|
458
|
+
const cached = auxConfigs.get(workerDef.configPath)
|
|
459
|
+
if (!cached) continue
|
|
460
|
+
const auxMgr = workerRegistry.getManager(cached.name)
|
|
461
|
+
if (!auxMgr) continue
|
|
462
|
+
for (const host of workerDef.hosts) {
|
|
463
|
+
hostRoutes.push({ pattern: host, manager: auxMgr, workerName: cached.name })
|
|
464
|
+
console.log(`[lopata:vite] Host route: ${host} → ${cached.name}`)
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
if (hostRoutes.length > 0) {
|
|
468
|
+
apiMod.setHostRoutes(hostRoutes.map(hr => ({ pattern: hr.pattern, workerName: hr.workerName })))
|
|
469
|
+
}
|
|
424
470
|
}
|
|
425
471
|
|
|
426
472
|
// 5. Set up WebSocket trace streaming on httpServer
|
|
@@ -471,26 +517,25 @@ export function devServerPlugin(options: DevServerPluginOptions): Plugin {
|
|
|
471
517
|
return
|
|
472
518
|
}
|
|
473
519
|
|
|
474
|
-
//
|
|
475
|
-
|
|
476
|
-
const
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
if (!routeDispatcher.isFallback(targetManager)) {
|
|
480
|
-
const gen = targetManager.active
|
|
520
|
+
// Aux worker dispatch: host-based first, then route-based
|
|
521
|
+
{
|
|
522
|
+
const resolved = resolveAuxWorker(req, url)
|
|
523
|
+
if (resolved) {
|
|
524
|
+
const gen = resolved.manager.active
|
|
481
525
|
if (!gen) {
|
|
482
526
|
if (!res.headersSent) {
|
|
483
527
|
res.writeHead(503, { 'content-type': 'text/plain' })
|
|
484
|
-
res.end('No active generation
|
|
528
|
+
res.end('No active generation')
|
|
485
529
|
}
|
|
486
530
|
return
|
|
487
531
|
}
|
|
488
532
|
try {
|
|
489
533
|
const request = nodeReqToRequest(req)
|
|
534
|
+
const parsedUrl = new URL(request.url)
|
|
490
535
|
const response = await (startSpan as Function)({
|
|
491
536
|
name: `${request.method} ${parsedUrl.pathname}`,
|
|
492
537
|
kind: 'server',
|
|
493
|
-
attributes: { 'http.method': request.method, 'http.url': request.url, 'lopata.worker':
|
|
538
|
+
attributes: { 'http.method': request.method, 'http.url': request.url, 'lopata.worker': resolved.workerName },
|
|
494
539
|
}, async () => {
|
|
495
540
|
const resp = await gen.callFetch(request, null) as Response
|
|
496
541
|
;(setSpanAttribute as Function)('http.status_code', resp.status)
|
|
@@ -655,37 +700,35 @@ export function devServerPlugin(options: DevServerPluginOptions): Plugin {
|
|
|
655
700
|
const request = nodeReqToRequest(req)
|
|
656
701
|
const parsedUrl = new URL(request.url)
|
|
657
702
|
|
|
658
|
-
//
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
socket.destroy()
|
|
682
|
-
return
|
|
683
|
-
}
|
|
684
|
-
wss.handleUpgrade(req, socket, head, (ws: any) => {
|
|
685
|
-
bridgeCfWebSocket(cfSocket, ws)
|
|
686
|
-
})
|
|
703
|
+
// Aux worker dispatch for WebSocket: host-based first, then route-based
|
|
704
|
+
const resolved = resolveAuxWorker(req, req.url ?? '/')
|
|
705
|
+
if (resolved) {
|
|
706
|
+
const gen = resolved.manager.active
|
|
707
|
+
if (!gen) {
|
|
708
|
+
socket.destroy()
|
|
709
|
+
return
|
|
710
|
+
}
|
|
711
|
+
const response = await (startSpan as Function)({
|
|
712
|
+
name: `WS ${parsedUrl.pathname}`,
|
|
713
|
+
kind: 'server',
|
|
714
|
+
attributes: {
|
|
715
|
+
'http.method': 'GET',
|
|
716
|
+
'http.url': request.url,
|
|
717
|
+
'lopata.worker': resolved.workerName,
|
|
718
|
+
'lopata.websocket': true,
|
|
719
|
+
},
|
|
720
|
+
}, async () => {
|
|
721
|
+
return gen.callFetch(request, null) as Promise<Response & { webSocket?: InstanceType<typeof CFWebSocket> }>
|
|
722
|
+
}) as Response & { webSocket?: InstanceType<typeof CFWebSocket> }
|
|
723
|
+
const cfSocket = response.webSocket
|
|
724
|
+
if (response.status !== 101 || !cfSocket || !(cfSocket instanceof CFWebSocket)) {
|
|
725
|
+
socket.destroy()
|
|
687
726
|
return
|
|
688
727
|
}
|
|
728
|
+
wss.handleUpgrade(req, socket, head, (ws: any) => {
|
|
729
|
+
bridgeCfWebSocket(cfSocket, ws)
|
|
730
|
+
})
|
|
731
|
+
return
|
|
689
732
|
}
|
|
690
733
|
|
|
691
734
|
const activeModule = await ensureWorkerModule()
|
package/src/vite-plugin/index.ts
CHANGED
|
@@ -11,7 +11,7 @@ export interface LopataPluginConfig {
|
|
|
11
11
|
/** Vite environment name for SSR. Default: "ssr" */
|
|
12
12
|
viteEnvironment?: { name?: string }
|
|
13
13
|
/** Auxiliary workers loaded via native Bun import (not through Vite). */
|
|
14
|
-
auxiliaryWorkers?: { configPath: string; name?: string }[]
|
|
14
|
+
auxiliaryWorkers?: { configPath: string; name?: string; hosts?: string[] }[]
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
/**
|