viteboard 0.1.2 → 0.1.4
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.
|
@@ -0,0 +1,722 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
const WORKSPACE_DIR = ".workspace";
|
|
7
|
+
const WORKSPACE_FILE = "workspace.json";
|
|
8
|
+
const IGNORE_DIRS = new Set([".git", "node_modules", "dist", "build", "coverage", ".next", "out"]);
|
|
9
|
+
const MIME = {
|
|
10
|
+
".png": "image/png",
|
|
11
|
+
".jpg": "image/jpeg",
|
|
12
|
+
".jpeg": "image/jpeg",
|
|
13
|
+
".webp": "image/webp",
|
|
14
|
+
".gif": "image/gif",
|
|
15
|
+
".svg": "image/svg+xml",
|
|
16
|
+
".md": "text/markdown; charset=utf-8",
|
|
17
|
+
".json": "application/json; charset=utf-8",
|
|
18
|
+
};
|
|
19
|
+
const MIME_EXT = {
|
|
20
|
+
"image/png": ".png",
|
|
21
|
+
"image/jpeg": ".jpg",
|
|
22
|
+
"image/webp": ".webp",
|
|
23
|
+
"image/gif": ".gif",
|
|
24
|
+
"image/svg+xml": ".svg",
|
|
25
|
+
};
|
|
26
|
+
const MANAGED_ASSET_RE = /^assets\/[a-f0-9]{64}\.(png|jpe?g|webp|gif|svg)$/i;
|
|
27
|
+
const ACTOR_TTL_MS = 30000;
|
|
28
|
+
const LOCK_TTL_MS = 30000;
|
|
29
|
+
const ROOT_LINK_HEADER = '</llms.txt>; rel="describedby"; type="text/plain", </.well-known/viteboard.json>; rel="alternate"; type="application/json"';
|
|
30
|
+
|
|
31
|
+
export function createContentApiPlugin() {
|
|
32
|
+
const cwd = process.cwd();
|
|
33
|
+
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
34
|
+
const workspaceDir = path.resolve(cwd, WORKSPACE_DIR);
|
|
35
|
+
const workspaceFile = path.join(workspaceDir, WORKSPACE_FILE);
|
|
36
|
+
const machineDir = path.join(packageRoot, "content", "machine");
|
|
37
|
+
const actorSessions = new Map();
|
|
38
|
+
const locks = new Map();
|
|
39
|
+
const sseClients = new Set();
|
|
40
|
+
|
|
41
|
+
function send(res, status, body) {
|
|
42
|
+
res.statusCode = status;
|
|
43
|
+
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
44
|
+
res.end(JSON.stringify(body));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function sendText(res, status, body, type) {
|
|
48
|
+
res.statusCode = status;
|
|
49
|
+
res.setHeader("Content-Type", type);
|
|
50
|
+
res.end(body);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function sendSse(res, event, body) {
|
|
54
|
+
res.write(`event: ${event}\n`);
|
|
55
|
+
res.write(`data: ${JSON.stringify(body)}\n\n`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function broadcast(event, body) {
|
|
59
|
+
for (const client of sseClients) {
|
|
60
|
+
try { sendSse(client, event, body); } catch { sseClients.delete(client); }
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function actorFromHeaders(req) {
|
|
65
|
+
const actorId = String(req.headers["x-viteboard-actor-id"] ?? "").trim();
|
|
66
|
+
if (!actorId) return null;
|
|
67
|
+
const actorType = String(req.headers["x-viteboard-actor-type"] ?? "human") === "agent" ? "agent" : "human";
|
|
68
|
+
const label = String(req.headers["x-viteboard-actor-label"] ?? "").trim() || (actorType === "agent" ? "Agent" : "ViteBoard UI");
|
|
69
|
+
return { actorId, actorType, label };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function lockKey(target) {
|
|
73
|
+
const suffix = target.targetType === "doc" ? target.path ?? "" : target.boardId ?? "";
|
|
74
|
+
return `${target.targetType}:${target.rootId}:${suffix}`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function activeActors() { return [...actorSessions.values()].sort((a, b) => a.label.localeCompare(b.label)); }
|
|
78
|
+
function activeLocks() { return [...locks.values()].sort((a, b) => a.acquiredAt - b.acquiredAt); }
|
|
79
|
+
function broadcastPresence() { broadcast("presence", { actors: activeActors(), locks: activeLocks() }); }
|
|
80
|
+
function broadcastLocks() { broadcast("lock-changed", { locks: activeLocks() }); }
|
|
81
|
+
|
|
82
|
+
function pruneEphemeral() {
|
|
83
|
+
const now = Date.now();
|
|
84
|
+
let changedPresence = false;
|
|
85
|
+
let changedLocks = false;
|
|
86
|
+
for (const [actorId, actor] of actorSessions) {
|
|
87
|
+
if (actor.expiresAt <= now) {
|
|
88
|
+
actorSessions.delete(actorId);
|
|
89
|
+
changedPresence = true;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
for (const [key, lock] of locks) {
|
|
93
|
+
if (lock.expiresAt <= now || !actorSessions.has(lock.actorId)) {
|
|
94
|
+
locks.delete(key);
|
|
95
|
+
changedLocks = true;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (changedPresence) broadcastPresence();
|
|
99
|
+
else if (changedLocks) broadcastLocks();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
setInterval(pruneEphemeral, 10000).unref?.();
|
|
103
|
+
|
|
104
|
+
function revisionForText(content) { return createHash("sha1").update(content).digest("hex"); }
|
|
105
|
+
function hashPath(value) {
|
|
106
|
+
let hash = 2166136261;
|
|
107
|
+
for (let i = 0; i < value.length; i++) {
|
|
108
|
+
hash ^= value.charCodeAt(i);
|
|
109
|
+
hash = Math.imul(hash, 16777619);
|
|
110
|
+
}
|
|
111
|
+
return (hash >>> 0).toString(36);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function readMachineAsset(fileName) { return fs.readFile(path.join(machineDir, fileName), "utf-8"); }
|
|
115
|
+
async function ensureWorkspaceDir() { await fs.mkdir(workspaceDir, { recursive: true }); }
|
|
116
|
+
async function readBody(req) {
|
|
117
|
+
return new Promise((resolve, reject) => {
|
|
118
|
+
let data = "";
|
|
119
|
+
req.on("data", (chunk) => {
|
|
120
|
+
data += chunk;
|
|
121
|
+
if (data.length > 64 * 1024 * 1024) {
|
|
122
|
+
reject(new Error("Payload too large"));
|
|
123
|
+
req.destroy();
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
req.on("end", () => resolve(data));
|
|
127
|
+
req.on("error", reject);
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function canonicalizeProjectRoot(rootPath) {
|
|
132
|
+
const resolved = path.resolve(rootPath);
|
|
133
|
+
try { return path.resolve(await fs.realpath(resolved)); } catch { return resolved; }
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function normalizeRelativePath(relPath) {
|
|
137
|
+
return relPath.replace(/\\/g, "/").replace(/^\/+|\/+$/g, "");
|
|
138
|
+
}
|
|
139
|
+
function dirnamePosix(filePath) {
|
|
140
|
+
const normalized = normalizeRelativePath(filePath);
|
|
141
|
+
const idx = normalized.lastIndexOf("/");
|
|
142
|
+
return idx === -1 ? "" : normalized.slice(0, idx);
|
|
143
|
+
}
|
|
144
|
+
function resolveDocRelativePath(fromFile, target) {
|
|
145
|
+
const raw = String(target ?? "").trim();
|
|
146
|
+
if (!raw || /^(https?:|mailto:|tel:|#|data:)/i.test(raw)) return null;
|
|
147
|
+
if (raw.startsWith("/")) return normalizeRelativePath(raw);
|
|
148
|
+
const base = dirnamePosix(fromFile);
|
|
149
|
+
return normalizeRelativePath(base ? `${base}/${raw}` : raw);
|
|
150
|
+
}
|
|
151
|
+
function isManagedAssetPath(relPath) { return MANAGED_ASSET_RE.test(normalizeRelativePath(relPath)); }
|
|
152
|
+
|
|
153
|
+
async function collectFiles(dir, base = "") {
|
|
154
|
+
let entries;
|
|
155
|
+
try { entries = await fs.readdir(dir, { withFileTypes: true }); } catch { return []; }
|
|
156
|
+
const files = [];
|
|
157
|
+
for (const entry of entries) {
|
|
158
|
+
if (entry.name.startsWith(".")) continue;
|
|
159
|
+
const rel = base ? `${base}/${entry.name}` : entry.name;
|
|
160
|
+
const abs = path.join(dir, entry.name);
|
|
161
|
+
if (entry.isDirectory()) files.push(...(await collectFiles(abs, rel)));
|
|
162
|
+
else files.push(rel);
|
|
163
|
+
}
|
|
164
|
+
return files;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function collectBoardAssetRefs(raw) {
|
|
168
|
+
const refs = new Set();
|
|
169
|
+
if (typeof raw !== "object" || raw === null) return refs;
|
|
170
|
+
const elements = Array.isArray(raw.elements) ? raw.elements : [];
|
|
171
|
+
for (const element of elements) {
|
|
172
|
+
if (typeof element !== "object" || element === null) continue;
|
|
173
|
+
const data = element.data;
|
|
174
|
+
const src = typeof data?.src === "string" ? normalizeRelativePath(data.src) : "";
|
|
175
|
+
if (isManagedAssetPath(src)) refs.add(src);
|
|
176
|
+
}
|
|
177
|
+
return refs;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function collectMarkdownAssetRefs(markdown, docPath) {
|
|
181
|
+
const refs = new Set();
|
|
182
|
+
const imagePatterns = [/!\[[^\]]*\]\(([^)\s]+)\)/g, /<img\b[^>]*\bsrc=["']([^"']+)["'][^>]*>/gi];
|
|
183
|
+
for (const pattern of imagePatterns) {
|
|
184
|
+
pattern.lastIndex = 0;
|
|
185
|
+
let match;
|
|
186
|
+
while ((match = pattern.exec(markdown))) {
|
|
187
|
+
const resolved = resolveDocRelativePath(docPath, match[1]);
|
|
188
|
+
if (resolved && isManagedAssetPath(resolved)) refs.add(resolved);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return refs;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function cleanupManagedAssets(rootAbsPath, preservePaths = []) {
|
|
195
|
+
const allFiles = await collectFiles(rootAbsPath);
|
|
196
|
+
const managedAssets = allFiles.filter((file) => isManagedAssetPath(file));
|
|
197
|
+
if (managedAssets.length === 0) return 0;
|
|
198
|
+
const referenced = new Set();
|
|
199
|
+
for (const relPath of allFiles) {
|
|
200
|
+
const abs = safeResolve(rootAbsPath, relPath);
|
|
201
|
+
if (!abs) continue;
|
|
202
|
+
if (relPath.endsWith(".board.json")) {
|
|
203
|
+
try {
|
|
204
|
+
const raw = JSON.parse(await fs.readFile(abs, "utf-8"));
|
|
205
|
+
for (const ref of collectBoardAssetRefs(raw)) referenced.add(ref);
|
|
206
|
+
} catch {}
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
if (relPath.endsWith(".md")) {
|
|
210
|
+
try {
|
|
211
|
+
const markdown = await fs.readFile(abs, "utf-8");
|
|
212
|
+
for (const ref of collectMarkdownAssetRefs(markdown, relPath)) referenced.add(ref);
|
|
213
|
+
} catch {}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
for (const relPath of preservePaths) {
|
|
217
|
+
const normalized = normalizeRelativePath(relPath);
|
|
218
|
+
if (isManagedAssetPath(normalized)) referenced.add(normalized);
|
|
219
|
+
}
|
|
220
|
+
let removed = 0;
|
|
221
|
+
for (const relPath of managedAssets) {
|
|
222
|
+
if (referenced.has(relPath)) continue;
|
|
223
|
+
const abs = safeResolve(rootAbsPath, relPath);
|
|
224
|
+
if (!abs) continue;
|
|
225
|
+
try { await fs.unlink(abs); removed++; } catch {}
|
|
226
|
+
}
|
|
227
|
+
return removed;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function isNestedPath(parent, child) {
|
|
231
|
+
const normalizedParent = parent.endsWith(path.sep) ? parent : parent + path.sep;
|
|
232
|
+
return child.startsWith(normalizedParent);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function overlapError(existing, candidateRoot) {
|
|
236
|
+
if (existing.rootPath === candidateRoot) return `"${existing.label}" is already added. Pick a different folder, or remove the existing project first.`;
|
|
237
|
+
if (isNestedPath(existing.rootPath, candidateRoot)) return `That folder is inside the existing workspace project "${existing.label}". ViteBoard only allows one project root per nested path.`;
|
|
238
|
+
return `That folder contains the existing workspace project "${existing.label}". Add the more specific project instead, or remove the current one first.`;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async function loadManifest() {
|
|
242
|
+
try {
|
|
243
|
+
const raw = await fs.readFile(workspaceFile, "utf-8");
|
|
244
|
+
const parsed = JSON.parse(raw);
|
|
245
|
+
const rawProjects = Array.isArray(parsed.projects) ? parsed.projects : [];
|
|
246
|
+
return {
|
|
247
|
+
version: 1,
|
|
248
|
+
projects: rawProjects.filter((project) => typeof project?.id === "string" && typeof project?.label === "string" && typeof project?.rootPath === "string"),
|
|
249
|
+
};
|
|
250
|
+
} catch { return { version: 1, projects: [] }; }
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async function saveManifest(manifest) {
|
|
254
|
+
await ensureWorkspaceDir();
|
|
255
|
+
await fs.writeFile(workspaceFile, JSON.stringify(manifest, null, 2) + "\n", "utf-8");
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function rootDisplayPath(projectRoot, docsAbsPath) {
|
|
259
|
+
const rel = path.relative(projectRoot, docsAbsPath).replace(/\\/g, "/");
|
|
260
|
+
return rel === "" ? "docs" : rel;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function docsRootId(projectId, absPath) { return `root-${hashPath(`${projectId}:${absPath}`)}`; }
|
|
264
|
+
function projectIdFor(absRoot) { return `project-${hashPath(absRoot)}`; }
|
|
265
|
+
|
|
266
|
+
async function discoverDocsRoots(project) {
|
|
267
|
+
const roots = [];
|
|
268
|
+
const walk = async (dir) => {
|
|
269
|
+
let entries;
|
|
270
|
+
try { entries = await fs.readdir(dir, { withFileTypes: true }); } catch { return; }
|
|
271
|
+
for (const entry of entries) {
|
|
272
|
+
if (entry.name.startsWith(".") || IGNORE_DIRS.has(entry.name)) continue;
|
|
273
|
+
const abs = path.join(dir, entry.name);
|
|
274
|
+
if (!entry.isDirectory()) continue;
|
|
275
|
+
if (entry.name === "docs") {
|
|
276
|
+
const relativePath = normalizeRelativePath(path.relative(project.rootPath, abs));
|
|
277
|
+
roots.push({
|
|
278
|
+
id: docsRootId(project.id, abs),
|
|
279
|
+
projectId: project.id,
|
|
280
|
+
projectLabel: project.label,
|
|
281
|
+
absPath: abs,
|
|
282
|
+
relativePath,
|
|
283
|
+
displayPath: rootDisplayPath(project.rootPath, abs),
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
await walk(abs);
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
await walk(project.rootPath);
|
|
290
|
+
return roots.sort((a, b) => a.displayPath.localeCompare(b.displayPath));
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
async function getWorkspace() {
|
|
294
|
+
const manifest = await loadManifest();
|
|
295
|
+
const projects = [];
|
|
296
|
+
for (const project of manifest.projects) projects.push({ ...project, docsRoots: await discoverDocsRoots(project) });
|
|
297
|
+
return projects;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function toWorkspaceResponse(projects) {
|
|
301
|
+
return {
|
|
302
|
+
projects: projects.map((project) => ({
|
|
303
|
+
id: project.id,
|
|
304
|
+
label: project.label,
|
|
305
|
+
rootPath: project.rootPath,
|
|
306
|
+
docsRoots: project.docsRoots.map((root) => ({
|
|
307
|
+
id: root.id,
|
|
308
|
+
projectId: root.projectId,
|
|
309
|
+
projectLabel: root.projectLabel,
|
|
310
|
+
relativePath: root.relativePath,
|
|
311
|
+
displayPath: root.displayPath,
|
|
312
|
+
})),
|
|
313
|
+
})),
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function safeResolve(root, rel) {
|
|
318
|
+
const abs = path.resolve(root, rel);
|
|
319
|
+
if (abs !== root && !abs.startsWith(root + path.sep)) return null;
|
|
320
|
+
return abs;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
async function resolveRoot(rootId) {
|
|
324
|
+
const projects = await getWorkspace();
|
|
325
|
+
for (const project of projects) {
|
|
326
|
+
const root = project.docsRoots.find((item) => item.id === rootId);
|
|
327
|
+
if (root) {
|
|
328
|
+
const nestedRoots = new Set(project.docsRoots.filter((item) => item.id !== root.id && item.absPath.startsWith(root.absPath + path.sep)).map((item) => item.relativePath));
|
|
329
|
+
return { root, nestedRoots };
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function boardIdFromFilePath(filePath) { return filePath.replace(/\.board\.json$/, ""); }
|
|
336
|
+
|
|
337
|
+
async function buildTree(rootAbsPath, nestedRoots = new Set(), base = "") {
|
|
338
|
+
let entries;
|
|
339
|
+
try { entries = await fs.readdir(path.join(rootAbsPath, base), { withFileTypes: true }); } catch { return []; }
|
|
340
|
+
const nodes = [];
|
|
341
|
+
for (const entry of entries) {
|
|
342
|
+
if (entry.name.startsWith(".") || IGNORE_DIRS.has(entry.name)) continue;
|
|
343
|
+
const rel = base ? `${base}/${entry.name}` : entry.name;
|
|
344
|
+
if (entry.isDirectory()) {
|
|
345
|
+
if (nestedRoots.has(rel)) continue;
|
|
346
|
+
nodes.push({ type: "dir", name: entry.name, path: rel, children: await buildTree(rootAbsPath, nestedRoots, rel) });
|
|
347
|
+
} else {
|
|
348
|
+
nodes.push({ type: "file", name: entry.name, path: rel });
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
nodes.sort((a, b) => a.name.localeCompare(b.name));
|
|
352
|
+
return nodes;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function firstMarkdownPath(nodes) {
|
|
356
|
+
for (const node of nodes) {
|
|
357
|
+
if (node.type === "file" && node.name.endsWith(".md")) return node.path;
|
|
358
|
+
if (node.type === "dir") {
|
|
359
|
+
const nested = firstMarkdownPath(node.children);
|
|
360
|
+
if (nested) return nested;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
async function pickProjectFromSystemDialog() {
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
async function sendMachineFile(res, fileName, contentType) {
|
|
371
|
+
const body = await readMachineAsset(fileName);
|
|
372
|
+
res.statusCode = 200;
|
|
373
|
+
res.setHeader("Content-Type", contentType);
|
|
374
|
+
res.setHeader("Link", ROOT_LINK_HEADER);
|
|
375
|
+
res.end(body);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
async function resolveWorkspaceProject(projectId) {
|
|
379
|
+
const manifest = await loadManifest();
|
|
380
|
+
return manifest.projects.find((project) => project.id === projectId) ?? null;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
async function ensureProjectDocsRoot(projectId) {
|
|
384
|
+
const project = await resolveWorkspaceProject(projectId);
|
|
385
|
+
if (!project) return null;
|
|
386
|
+
const existing = (await discoverDocsRoots(project))[0];
|
|
387
|
+
if (existing) return existing;
|
|
388
|
+
const abs = path.join(project.rootPath, "docs");
|
|
389
|
+
await fs.mkdir(abs, { recursive: true });
|
|
390
|
+
return {
|
|
391
|
+
id: docsRootId(project.id, abs),
|
|
392
|
+
projectId: project.id,
|
|
393
|
+
projectLabel: project.label,
|
|
394
|
+
absPath: abs,
|
|
395
|
+
relativePath: "docs",
|
|
396
|
+
displayPath: "docs",
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
async function createDocsFolder(projectId, parentPath) {
|
|
401
|
+
const project = await resolveWorkspaceProject(projectId);
|
|
402
|
+
if (!project) return null;
|
|
403
|
+
const parentAbs = safeResolve(project.rootPath, parentPath || ".");
|
|
404
|
+
if (!parentAbs) return null;
|
|
405
|
+
const abs = path.join(parentAbs, "docs");
|
|
406
|
+
await fs.mkdir(abs, { recursive: true });
|
|
407
|
+
return {
|
|
408
|
+
id: docsRootId(project.id, abs),
|
|
409
|
+
projectId: project.id,
|
|
410
|
+
projectLabel: project.label,
|
|
411
|
+
absPath: abs,
|
|
412
|
+
relativePath: normalizeRelativePath(path.relative(project.rootPath, abs)),
|
|
413
|
+
displayPath: rootDisplayPath(project.rootPath, abs),
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
async function deleteEmptyParentDirs(startDir, stopDir) {
|
|
418
|
+
let dir = startDir;
|
|
419
|
+
while (dir.startsWith(stopDir) && dir !== stopDir) {
|
|
420
|
+
try {
|
|
421
|
+
const entries = await fs.readdir(dir);
|
|
422
|
+
if (entries.length > 0) break;
|
|
423
|
+
await fs.rmdir(dir);
|
|
424
|
+
} catch { break; }
|
|
425
|
+
dir = path.dirname(dir);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
async function renamePath(rootAbsPath, oldPath, newPath, ignoreMissing = false) {
|
|
430
|
+
const oldAbs = safeResolve(rootAbsPath, oldPath);
|
|
431
|
+
const newAbs = safeResolve(rootAbsPath, newPath);
|
|
432
|
+
if (!oldAbs || !newAbs) throw new Error("Invalid path");
|
|
433
|
+
try { await fs.access(oldAbs); } catch (err) {
|
|
434
|
+
if (ignoreMissing && err.code === "ENOENT") return;
|
|
435
|
+
throw err;
|
|
436
|
+
}
|
|
437
|
+
await fs.mkdir(path.dirname(newAbs), { recursive: true });
|
|
438
|
+
await fs.rename(oldAbs, newAbs);
|
|
439
|
+
await deleteEmptyParentDirs(path.dirname(oldAbs), rootAbsPath);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
async function handler(req, res, next) {
|
|
443
|
+
pruneEphemeral();
|
|
444
|
+
const actor = actorFromHeaders(req);
|
|
445
|
+
const url = req.originalUrl || req.url || "/";
|
|
446
|
+
const parsed = new URL(url, "http://localhost");
|
|
447
|
+
const route = parsed.pathname;
|
|
448
|
+
const method = req.method || "GET";
|
|
449
|
+
if (route === "/llms.txt" && method === "GET") return void (await sendMachineFile(res, "llms.txt", "text/plain; charset=utf-8"));
|
|
450
|
+
if (route === "/viteboard-guide.md" && method === "GET") return void (await sendMachineFile(res, "guide.md", "text/markdown; charset=utf-8"));
|
|
451
|
+
if (route === "/board-v1.schema.json" && method === "GET") return void (await sendMachineFile(res, "board-v1.schema.json", "application/schema+json; charset=utf-8"));
|
|
452
|
+
if (route === "/viteboard-example.board.json" && method === "GET") return void (await sendMachineFile(res, "example.board.json", "application/json; charset=utf-8"));
|
|
453
|
+
if (route === "/.well-known/viteboard.json" && method === "GET") return void (await sendMachineFile(res, "viteboard.json", "application/json; charset=utf-8"));
|
|
454
|
+
if (!route.startsWith("/__api/")) return void next();
|
|
455
|
+
res.setHeader("Link", ROOT_LINK_HEADER);
|
|
456
|
+
|
|
457
|
+
try {
|
|
458
|
+
if (route === "/__api/events" && method === "GET") {
|
|
459
|
+
res.statusCode = 200;
|
|
460
|
+
res.setHeader("Content-Type", "text/event-stream; charset=utf-8");
|
|
461
|
+
res.setHeader("Cache-Control", "no-cache, no-transform");
|
|
462
|
+
res.setHeader("Connection", "keep-alive");
|
|
463
|
+
res.flushHeaders?.();
|
|
464
|
+
sseClients.add(res);
|
|
465
|
+
sendSse(res, "hello", { actors: activeActors(), locks: activeLocks() });
|
|
466
|
+
req.on("close", () => sseClients.delete(res));
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
if (route === "/__api/workspace" && method === "GET") return send(res, 200, await toWorkspaceResponse(await getWorkspace()));
|
|
471
|
+
if (route === "/__api/project-tree" && method === "GET") {
|
|
472
|
+
const projectId = parsed.searchParams.get("projectId") ?? "";
|
|
473
|
+
const project = await resolveWorkspaceProject(projectId);
|
|
474
|
+
if (!project) return send(res, 404, { error: "Project not found" });
|
|
475
|
+
return send(res, 200, { tree: await buildTree(project.rootPath) });
|
|
476
|
+
}
|
|
477
|
+
if (route === "/__api/tree" && method === "GET") {
|
|
478
|
+
const rootId = parsed.searchParams.get("rootId") ?? "";
|
|
479
|
+
const resolved = await resolveRoot(rootId);
|
|
480
|
+
if (!resolved) return send(res, 404, { error: "Docs root not found" });
|
|
481
|
+
return send(res, 200, { tree: await buildTree(resolved.root.absPath, resolved.nestedRoots) });
|
|
482
|
+
}
|
|
483
|
+
if (route === "/__api/file" && method === "GET") {
|
|
484
|
+
const rootId = parsed.searchParams.get("rootId") ?? "";
|
|
485
|
+
const rel = parsed.searchParams.get("path") ?? "";
|
|
486
|
+
const resolved = await resolveRoot(rootId);
|
|
487
|
+
if (!resolved) return send(res, 404, { error: "Docs root not found" });
|
|
488
|
+
const abs = safeResolve(resolved.root.absPath, rel);
|
|
489
|
+
if (!abs) return send(res, 400, { error: "Invalid path" });
|
|
490
|
+
try {
|
|
491
|
+
const content = await fs.readFile(abs, "utf-8");
|
|
492
|
+
return send(res, 200, { path: rel, content, revision: revisionForText(content) });
|
|
493
|
+
} catch { return send(res, 404, { error: "Not found" }); }
|
|
494
|
+
}
|
|
495
|
+
if (route === "/__api/file" && method === "PUT") {
|
|
496
|
+
const { rootId, path: rel, content, expectedRevision } = JSON.parse(await readBody(req));
|
|
497
|
+
const resolved = await resolveRoot(String(rootId ?? ""));
|
|
498
|
+
if (!resolved) return send(res, 404, { error: "Docs root not found" });
|
|
499
|
+
const abs = safeResolve(resolved.root.absPath, String(rel ?? ""));
|
|
500
|
+
if (!abs) return send(res, 400, { error: "Invalid path" });
|
|
501
|
+
try {
|
|
502
|
+
const current = await fs.readFile(abs, "utf-8").catch((err) => err.code === "ENOENT" ? null : Promise.reject(err));
|
|
503
|
+
if (expectedRevision && current !== null && revisionForText(current) !== expectedRevision) return send(res, 409, { error: "File changed on disk", currentRevision: revisionForText(current) });
|
|
504
|
+
await fs.mkdir(path.dirname(abs), { recursive: true });
|
|
505
|
+
const nextContent = String(content ?? "");
|
|
506
|
+
await fs.writeFile(abs, nextContent, "utf-8");
|
|
507
|
+
const revision = revisionForText(nextContent);
|
|
508
|
+
broadcast("file-changed", { rootId: resolved.root.id, path: String(rel), action: "write", actorId: actor?.actorId ?? null });
|
|
509
|
+
return send(res, 200, { ok: true, path: rel, revision });
|
|
510
|
+
} catch (err) { return send(res, 500, { error: err.message }); }
|
|
511
|
+
}
|
|
512
|
+
if (route === "/__api/file" && method === "PATCH") {
|
|
513
|
+
const { rootId, oldPath, newPath, ignoreMissing } = JSON.parse(await readBody(req));
|
|
514
|
+
const resolved = await resolveRoot(String(rootId ?? ""));
|
|
515
|
+
if (!resolved) return send(res, 404, { error: "Docs root not found" });
|
|
516
|
+
await renamePath(resolved.root.absPath, String(oldPath ?? ""), String(newPath ?? ""), !!ignoreMissing);
|
|
517
|
+
broadcast("file-changed", { rootId: resolved.root.id, path: String(newPath), action: "rename", actorId: actor?.actorId ?? null });
|
|
518
|
+
return send(res, 200, { ok: true });
|
|
519
|
+
}
|
|
520
|
+
if (route === "/__api/file" && method === "DELETE") {
|
|
521
|
+
const rootId = parsed.searchParams.get("rootId") ?? "";
|
|
522
|
+
const rel = parsed.searchParams.get("path") ?? "";
|
|
523
|
+
const resolved = await resolveRoot(rootId);
|
|
524
|
+
if (!resolved) return send(res, 404, { error: "Docs root not found" });
|
|
525
|
+
const abs = safeResolve(resolved.root.absPath, rel);
|
|
526
|
+
if (!abs) return send(res, 400, { error: "Invalid path" });
|
|
527
|
+
try { await fs.unlink(abs); } catch (err) { if (err.code !== "ENOENT") throw err; }
|
|
528
|
+
await deleteEmptyParentDirs(path.dirname(abs), resolved.root.absPath);
|
|
529
|
+
broadcast("file-changed", { rootId: resolved.root.id, path: rel, action: "delete", actorId: actor?.actorId ?? null });
|
|
530
|
+
return send(res, 200, { ok: true });
|
|
531
|
+
}
|
|
532
|
+
if (route === "/__api/directory" && method === "PUT") {
|
|
533
|
+
const { rootId, path: rel } = JSON.parse(await readBody(req));
|
|
534
|
+
const resolved = await resolveRoot(String(rootId ?? ""));
|
|
535
|
+
if (!resolved) return send(res, 404, { error: "Docs root not found" });
|
|
536
|
+
const abs = safeResolve(resolved.root.absPath, String(rel ?? ""));
|
|
537
|
+
if (!abs) return send(res, 400, { error: "Invalid path" });
|
|
538
|
+
await fs.mkdir(abs, { recursive: true });
|
|
539
|
+
broadcast("file-changed", { rootId: resolved.root.id, path: String(rel), action: "mkdir", actorId: actor?.actorId ?? null });
|
|
540
|
+
return send(res, 200, { ok: true });
|
|
541
|
+
}
|
|
542
|
+
if (route === "/__api/board" && method === "GET") {
|
|
543
|
+
const rootId = parsed.searchParams.get("rootId") ?? "";
|
|
544
|
+
const id = parsed.searchParams.get("id") ?? "";
|
|
545
|
+
const resolved = await resolveRoot(rootId);
|
|
546
|
+
if (!resolved) return send(res, 404, { error: "Docs root not found" });
|
|
547
|
+
const abs = safeResolve(resolved.root.absPath, `${id}.board.json`);
|
|
548
|
+
if (!abs) return send(res, 400, { error: "Invalid id" });
|
|
549
|
+
try {
|
|
550
|
+
const content = await fs.readFile(abs, "utf-8");
|
|
551
|
+
return send(res, 200, { id, doc: JSON.parse(content), revision: revisionForText(content) });
|
|
552
|
+
} catch { return send(res, 404, { error: "Not found" }); }
|
|
553
|
+
}
|
|
554
|
+
if (route === "/__api/board" && method === "PUT") {
|
|
555
|
+
const { rootId, id, doc, expectedRevision } = JSON.parse(await readBody(req));
|
|
556
|
+
const resolved = await resolveRoot(String(rootId ?? ""));
|
|
557
|
+
if (!resolved) return send(res, 404, { error: "Docs root not found" });
|
|
558
|
+
const abs = safeResolve(resolved.root.absPath, `${id}.board.json`);
|
|
559
|
+
if (!abs) return send(res, 400, { error: "Invalid id" });
|
|
560
|
+
const current = await fs.readFile(abs, "utf-8").catch((err) => err.code === "ENOENT" ? null : Promise.reject(err));
|
|
561
|
+
if (expectedRevision && current !== null && revisionForText(current) !== expectedRevision) return send(res, 409, { error: "Board changed on disk", currentRevision: revisionForText(current) });
|
|
562
|
+
await fs.mkdir(path.dirname(abs), { recursive: true });
|
|
563
|
+
const nextContent = JSON.stringify(doc, null, 2) + "\n";
|
|
564
|
+
await fs.writeFile(abs, nextContent, "utf-8");
|
|
565
|
+
const revision = revisionForText(nextContent);
|
|
566
|
+
broadcast("board-changed", { rootId: resolved.root.id, boardId: String(id), action: "write", actorId: actor?.actorId ?? null });
|
|
567
|
+
return send(res, 200, { ok: true, id, revision });
|
|
568
|
+
}
|
|
569
|
+
if (route === "/__api/board" && method === "DELETE") {
|
|
570
|
+
const rootId = parsed.searchParams.get("rootId") ?? "";
|
|
571
|
+
const id = parsed.searchParams.get("id") ?? "";
|
|
572
|
+
const resolved = await resolveRoot(rootId);
|
|
573
|
+
if (!resolved) return send(res, 404, { error: "Docs root not found" });
|
|
574
|
+
const abs = safeResolve(resolved.root.absPath, `${id}.board.json`);
|
|
575
|
+
if (!abs) return send(res, 400, { error: "Invalid id" });
|
|
576
|
+
try { await fs.unlink(abs); } catch (err) { if (err.code !== "ENOENT") throw err; }
|
|
577
|
+
broadcast("board-changed", { rootId: resolved.root.id, boardId: id, action: "delete", actorId: actor?.actorId ?? null });
|
|
578
|
+
return send(res, 200, { ok: true, id });
|
|
579
|
+
}
|
|
580
|
+
if (route === "/__api/asset" && method === "GET") {
|
|
581
|
+
const rootId = parsed.searchParams.get("rootId") ?? "";
|
|
582
|
+
const rel = parsed.searchParams.get("path") ?? "";
|
|
583
|
+
const resolved = await resolveRoot(rootId);
|
|
584
|
+
if (!resolved) return send(res, 404, { error: "Docs root not found" });
|
|
585
|
+
const abs = safeResolve(resolved.root.absPath, rel);
|
|
586
|
+
if (!abs) return send(res, 400, { error: "Invalid path" });
|
|
587
|
+
try {
|
|
588
|
+
const buf = await fs.readFile(abs);
|
|
589
|
+
res.statusCode = 200;
|
|
590
|
+
res.setHeader("Content-Type", MIME[path.extname(abs).toLowerCase()] ?? "application/octet-stream");
|
|
591
|
+
res.setHeader("Cache-Control", "no-cache");
|
|
592
|
+
return res.end(buf);
|
|
593
|
+
} catch { return send(res, 404, { error: "Not found" }); }
|
|
594
|
+
}
|
|
595
|
+
if (route === "/__api/asset" && method === "POST") {
|
|
596
|
+
const { rootId, name, base64, mimeType } = JSON.parse(await readBody(req));
|
|
597
|
+
const resolved = await resolveRoot(String(rootId ?? ""));
|
|
598
|
+
if (!resolved) return send(res, 404, { error: "Docs root not found" });
|
|
599
|
+
const bytes = Buffer.from(String(base64), "base64");
|
|
600
|
+
const ext = MIME_EXT[String(mimeType ?? "")] ?? (path.extname(String(name ?? "")) || ".png").toLowerCase();
|
|
601
|
+
const digest = createHash("sha256").update(bytes).digest("hex");
|
|
602
|
+
const rel = `assets/${digest}${ext}`;
|
|
603
|
+
const abs = safeResolve(resolved.root.absPath, rel);
|
|
604
|
+
if (!abs) return send(res, 400, { error: "Invalid path" });
|
|
605
|
+
await fs.mkdir(path.dirname(abs), { recursive: true });
|
|
606
|
+
try { await fs.access(abs); } catch { await fs.writeFile(abs, bytes); }
|
|
607
|
+
broadcast("asset-changed", { rootId: resolved.root.id, path: rel, action: "write", actorId: actor?.actorId ?? null });
|
|
608
|
+
return send(res, 200, { ok: true, path: rel });
|
|
609
|
+
}
|
|
610
|
+
if (route === "/__api/asset/cleanup" && method === "POST") {
|
|
611
|
+
const { rootId, preservePaths } = JSON.parse(await readBody(req));
|
|
612
|
+
const resolved = await resolveRoot(String(rootId ?? ""));
|
|
613
|
+
if (!resolved) return send(res, 404, { error: "Docs root not found" });
|
|
614
|
+
const removed = await cleanupManagedAssets(resolved.root.absPath, Array.isArray(preservePaths) ? preservePaths.filter((item) => typeof item === "string") : []);
|
|
615
|
+
if (removed > 0) broadcast("asset-changed", { rootId: resolved.root.id, action: "cleanup", removed, actorId: actor?.actorId ?? null });
|
|
616
|
+
return send(res, 200, { ok: true, removed });
|
|
617
|
+
}
|
|
618
|
+
if (route === "/__api/workspace-file" && method === "GET") {
|
|
619
|
+
await ensureWorkspaceDir();
|
|
620
|
+
const manifest = await loadManifest();
|
|
621
|
+
return sendText(res, 200, JSON.stringify(manifest, null, 2) + "\n", "application/json; charset=utf-8");
|
|
622
|
+
}
|
|
623
|
+
if (route === "/__api/workspace/project" && method === "POST") {
|
|
624
|
+
const { rootPath, label } = JSON.parse(await readBody(req));
|
|
625
|
+
const absRoot = await canonicalizeProjectRoot(String(rootPath ?? ""));
|
|
626
|
+
const manifest = await loadManifest();
|
|
627
|
+
const projectId = projectIdFor(absRoot);
|
|
628
|
+
const overlap = manifest.projects.find((project) => project.id !== projectId && (isNestedPath(project.rootPath, absRoot) || isNestedPath(absRoot, project.rootPath)));
|
|
629
|
+
if (overlap) return send(res, 409, { error: overlapError(overlap, absRoot) });
|
|
630
|
+
if (!manifest.projects.some((project) => project.id === projectId)) {
|
|
631
|
+
manifest.projects.push({ id: projectId, label: String(label ?? (path.basename(absRoot) || "Project")), rootPath: absRoot });
|
|
632
|
+
await saveManifest(manifest);
|
|
633
|
+
}
|
|
634
|
+
return send(res, 200, await toWorkspaceResponse(await getWorkspace()));
|
|
635
|
+
}
|
|
636
|
+
if (route === "/__api/workspace/project" && method === "DELETE") {
|
|
637
|
+
const projectId = parsed.searchParams.get("id") ?? "";
|
|
638
|
+
const manifest = await loadManifest();
|
|
639
|
+
manifest.projects = manifest.projects.filter((project) => project.id !== projectId);
|
|
640
|
+
await saveManifest(manifest);
|
|
641
|
+
return send(res, 200, { ok: true, workspace: await toWorkspaceResponse(await getWorkspace()) });
|
|
642
|
+
}
|
|
643
|
+
if (route === "/__api/workspace/pick-project" && method === "POST") {
|
|
644
|
+
const picked = await pickProjectFromSystemDialog();
|
|
645
|
+
if (!picked) return send(res, 200, { cancelled: true, workspace: await toWorkspaceResponse(await getWorkspace()) });
|
|
646
|
+
const absRoot = await canonicalizeProjectRoot(picked);
|
|
647
|
+
const manifest = await loadManifest();
|
|
648
|
+
const projectId = projectIdFor(absRoot);
|
|
649
|
+
const overlap = manifest.projects.find((project) => project.id !== projectId && (isNestedPath(project.rootPath, absRoot) || isNestedPath(absRoot, project.rootPath)));
|
|
650
|
+
if (overlap) return send(res, 409, { error: overlapError(overlap, absRoot) });
|
|
651
|
+
if (!manifest.projects.some((project) => project.id === projectId)) {
|
|
652
|
+
manifest.projects.push({ id: projectId, label: path.basename(absRoot) || "Project", rootPath: absRoot });
|
|
653
|
+
await saveManifest(manifest);
|
|
654
|
+
}
|
|
655
|
+
const workspace = await getWorkspace();
|
|
656
|
+
return send(res, 200, { cancelled: false, workspace: await toWorkspaceResponse(workspace), addedProjectId: projectId });
|
|
657
|
+
}
|
|
658
|
+
if (route === "/__api/workspace/project/docs-root" && method === "POST") {
|
|
659
|
+
const { projectId } = JSON.parse(await readBody(req));
|
|
660
|
+
const root = await ensureProjectDocsRoot(String(projectId ?? ""));
|
|
661
|
+
if (!root) return send(res, 404, { error: "Project not found" });
|
|
662
|
+
const workspace = await getWorkspace();
|
|
663
|
+
return send(res, 200, { root: { id: root.id, projectId: root.projectId, projectLabel: root.projectLabel, relativePath: root.relativePath, displayPath: root.displayPath }, workspace: await toWorkspaceResponse(workspace) });
|
|
664
|
+
}
|
|
665
|
+
if (route === "/__api/workspace/project/create-docs-folder" && method === "POST") {
|
|
666
|
+
const { projectId, parentPath } = JSON.parse(await readBody(req));
|
|
667
|
+
const root = await createDocsFolder(String(projectId ?? ""), String(parentPath ?? ""));
|
|
668
|
+
if (!root) return send(res, 404, { error: "Project not found" });
|
|
669
|
+
const workspace = await getWorkspace();
|
|
670
|
+
return send(res, 200, { root: { id: root.id, projectId: root.projectId, projectLabel: root.projectLabel, relativePath: root.relativePath, displayPath: root.displayPath }, workspace: await toWorkspaceResponse(workspace) });
|
|
671
|
+
}
|
|
672
|
+
if (route === "/__api/actors/session" && method === "POST") {
|
|
673
|
+
const payload = JSON.parse(await readBody(req));
|
|
674
|
+
const now = Date.now();
|
|
675
|
+
actorSessions.set(payload.actorId, { ...payload, updatedAt: now, expiresAt: now + ACTOR_TTL_MS });
|
|
676
|
+
broadcastPresence();
|
|
677
|
+
return send(res, 200, { ok: true });
|
|
678
|
+
}
|
|
679
|
+
if (route === "/__api/actors/heartbeat" && method === "POST") {
|
|
680
|
+
const payload = JSON.parse(await readBody(req));
|
|
681
|
+
const now = Date.now();
|
|
682
|
+
actorSessions.set(payload.actorId, { ...payload, updatedAt: now, expiresAt: now + ACTOR_TTL_MS });
|
|
683
|
+
broadcastPresence();
|
|
684
|
+
return send(res, 200, { ok: true });
|
|
685
|
+
}
|
|
686
|
+
if (route === "/__api/actors/session" && method === "DELETE") {
|
|
687
|
+
const actorId = parsed.searchParams.get("actorId") ?? "";
|
|
688
|
+
actorSessions.delete(actorId);
|
|
689
|
+
for (const [key, lock] of locks) if (lock.actorId === actorId) locks.delete(key);
|
|
690
|
+
broadcastPresence();
|
|
691
|
+
return send(res, 200, { ok: true });
|
|
692
|
+
}
|
|
693
|
+
if (route === "/__api/locks/acquire" && method === "POST") {
|
|
694
|
+
const target = JSON.parse(await readBody(req));
|
|
695
|
+
if (!actor) return send(res, 400, { error: "Missing actor context" });
|
|
696
|
+
const key = lockKey(target);
|
|
697
|
+
const existing = locks.get(key);
|
|
698
|
+
if (existing && existing.actorId !== actor.actorId) return send(res, 409, { error: "Target already locked", lock: existing });
|
|
699
|
+
const now = Date.now();
|
|
700
|
+
const lock = { ...target, lockId: `${key}:${now}`, actorId: actor.actorId, actorType: actor.actorType, label: actor.label, acquiredAt: now, expiresAt: now + LOCK_TTL_MS };
|
|
701
|
+
locks.set(key, lock);
|
|
702
|
+
broadcastLocks();
|
|
703
|
+
return send(res, 200, { lock });
|
|
704
|
+
}
|
|
705
|
+
if (route === "/__api/locks/release" && method === "POST") {
|
|
706
|
+
const target = JSON.parse(await readBody(req));
|
|
707
|
+
locks.delete(lockKey(target));
|
|
708
|
+
broadcastLocks();
|
|
709
|
+
return send(res, 200, { ok: true });
|
|
710
|
+
}
|
|
711
|
+
return send(res, 404, { error: "Unknown endpoint" });
|
|
712
|
+
} catch (err) {
|
|
713
|
+
return send(res, 500, { error: err.message });
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
return {
|
|
718
|
+
name: "viteboard-content-api",
|
|
719
|
+
configureServer(server) { server.middlewares.use(handler); },
|
|
720
|
+
configurePreviewServer(server) { server.middlewares.use(handler); },
|
|
721
|
+
};
|
|
722
|
+
}
|
package/bin/viteboard.mjs
CHANGED
|
@@ -4,6 +4,7 @@ import { createServer } from "vite";
|
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import fs from "node:fs";
|
|
7
|
+
import { createContentApiPlugin } from "./content-api-plugin.mjs";
|
|
7
8
|
|
|
8
9
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
10
|
const packageRoot = path.resolve(__dirname, "..");
|
|
@@ -29,15 +30,10 @@ async function main() {
|
|
|
29
30
|
await server.listen();
|
|
30
31
|
server.printUrls();
|
|
31
32
|
} else if (hasDist) {
|
|
32
|
-
// Installed as a package — serve the pre-built dist with the
|
|
33
|
-
// We need to dynamically import the config since it may be .ts
|
|
34
|
-
const { default: configFn } = await import(path.join(packageRoot, "vite.config.ts"));
|
|
35
|
-
const config = typeof configFn === "function" ? configFn({}) : configFn;
|
|
36
|
-
const plugins = Array.isArray(config.plugins) ? config.plugins : [];
|
|
37
|
-
|
|
33
|
+
// Installed as a package — serve the pre-built dist with the runtime JS plugin.
|
|
38
34
|
const server = await createServer({
|
|
39
35
|
root: path.join(packageRoot, "dist"),
|
|
40
|
-
plugins,
|
|
36
|
+
plugins: [createContentApiPlugin()],
|
|
41
37
|
server: { port, host, open: true },
|
|
42
38
|
});
|
|
43
39
|
await server.listen();
|
package/package.json
CHANGED