@decocms/tanstack 7.0.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/package.json +48 -0
- package/src/cms/kvBlockSource.test.ts +67 -0
- package/src/cms/kvBlockSource.ts +54 -0
- package/src/daemon/auth.ts +204 -0
- package/src/daemon/fs.ts +238 -0
- package/src/daemon/index.ts +8 -0
- package/src/daemon/middleware.ts +156 -0
- package/src/daemon/tunnel.ts +129 -0
- package/src/daemon/volumes.ts +366 -0
- package/src/daemon/watch.ts +272 -0
- package/src/hooks/DecoPageRenderer.tsx +685 -0
- package/src/hooks/DecoRootLayout.tsx +111 -0
- package/src/hooks/NavigationProgress.tsx +21 -0
- package/src/hooks/PreviewProviders.tsx +26 -0
- package/src/hooks/StableOutlet.tsx +30 -0
- package/src/hooks/index.ts +9 -0
- package/src/index.ts +30 -0
- package/src/routes/adminRoutes.ts +114 -0
- package/src/routes/cmsRoute.ts +706 -0
- package/src/routes/components.tsx +57 -0
- package/src/routes/index.ts +24 -0
- package/src/routes/pageUrl.test.ts +70 -0
- package/src/routes/pageUrl.ts +54 -0
- package/src/routes/withSiteGlobals.test.ts +206 -0
- package/src/routes/withSiteGlobals.ts +222 -0
- package/src/sdk/cookiePassthrough.ts +58 -0
- package/src/sdk/createInvoke.ts +57 -0
- package/src/sdk/kvHydration.test.ts +171 -0
- package/src/sdk/kvHydration.ts +176 -0
- package/src/sdk/router.ts +92 -0
- package/src/sdk/useHydrated.ts +19 -0
- package/src/sdk/workerEntry.test.ts +106 -0
- package/src/sdk/workerEntry.ts +1967 -0
- package/src/setupFastDeploy.ts +13 -0
- package/src/vite/plugin.js +646 -0
- package/src/vite/plugin.test.js +113 -0
- package/tsconfig.json +7 -0
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSE endpoint for file change events — initial sync + live updates.
|
|
3
|
+
*
|
|
4
|
+
* Ported from: deco-cx/deco daemon/sse/api.ts + daemon/sse/channel.ts
|
|
5
|
+
*/
|
|
6
|
+
import { readdir, readFile, stat } from "node:fs/promises";
|
|
7
|
+
import { join, sep } from "node:path";
|
|
8
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
9
|
+
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Event types — simplified from daemon/fs/common.ts
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
interface FSEvent {
|
|
15
|
+
type: "fs-sync" | "fs-snapshot" | "worker-status" | "meta-info";
|
|
16
|
+
detail: Record<string, unknown>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Broadcast channel (EventTarget-based, same as daemon/sse/channel.ts)
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
const channel = new EventTarget();
|
|
24
|
+
|
|
25
|
+
export function broadcastFSEvent(event: FSEvent): void {
|
|
26
|
+
channel.dispatchEvent(new CustomEvent("broadcast", { detail: event }));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// Helpers
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
const toPosix = (p: string) => p.replaceAll(sep, "/");
|
|
34
|
+
|
|
35
|
+
function shouldIgnore(path: string): boolean {
|
|
36
|
+
return (
|
|
37
|
+
path.includes(`${sep}.git${sep}`) ||
|
|
38
|
+
path.includes(`${sep}node_modules${sep}`) ||
|
|
39
|
+
path.includes(`${sep}.agent-home${sep}`) ||
|
|
40
|
+
path.includes(`${sep}.claude${sep}`)
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Infer block type from __resolveType string.
|
|
46
|
+
* Maps to manifest block categories (pages, sections, loaders, etc.).
|
|
47
|
+
*/
|
|
48
|
+
function inferBlockType(resolveType: string): string | null {
|
|
49
|
+
if (!resolveType) return null;
|
|
50
|
+
if (resolveType.includes("/pages/")) return "pages";
|
|
51
|
+
if (resolveType.includes("/sections/")) return "sections";
|
|
52
|
+
if (resolveType.includes("/loaders/")) return "loaders";
|
|
53
|
+
if (resolveType.includes("/actions/")) return "actions";
|
|
54
|
+
if (resolveType.includes("/matchers/")) return "matchers";
|
|
55
|
+
if (resolveType.includes("/flags/")) return "sections";
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface Metadata {
|
|
60
|
+
kind: "block" | "file";
|
|
61
|
+
blockType?: string;
|
|
62
|
+
__resolveType?: string;
|
|
63
|
+
name?: string;
|
|
64
|
+
path?: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Read a JSON file and infer its metadata (block type, resolveType, etc.).
|
|
69
|
+
* Matches the Deno daemon's inferMetadata from daemon/fs/api.ts.
|
|
70
|
+
*/
|
|
71
|
+
export async function inferMetadata(filepath: string): Promise<Metadata | null> {
|
|
72
|
+
try {
|
|
73
|
+
const raw = await readFile(filepath, "utf-8");
|
|
74
|
+
const parsed = JSON.parse(raw);
|
|
75
|
+
const { __resolveType, name, path: pagePath } = parsed;
|
|
76
|
+
|
|
77
|
+
if (!__resolveType) return { kind: "file" };
|
|
78
|
+
|
|
79
|
+
const blockType = inferBlockType(__resolveType);
|
|
80
|
+
if (!blockType) return { kind: "file" };
|
|
81
|
+
|
|
82
|
+
if (blockType === "pages") {
|
|
83
|
+
return {
|
|
84
|
+
kind: "block",
|
|
85
|
+
blockType,
|
|
86
|
+
__resolveType,
|
|
87
|
+
name: name ?? undefined,
|
|
88
|
+
path: pagePath ?? undefined,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return { kind: "block", blockType, __resolveType };
|
|
93
|
+
} catch {
|
|
94
|
+
return { kind: "file" };
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
// Initial file scan — yields fs-sync events for each .deco/ file
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
async function* scanFiles(
|
|
103
|
+
cwd: string,
|
|
104
|
+
since: number,
|
|
105
|
+
): AsyncGenerator<FSEvent> {
|
|
106
|
+
const decoDir = join(cwd, ".deco");
|
|
107
|
+
try {
|
|
108
|
+
const entries = await readdir(decoDir, {
|
|
109
|
+
recursive: true,
|
|
110
|
+
withFileTypes: true,
|
|
111
|
+
});
|
|
112
|
+
for (const entry of entries) {
|
|
113
|
+
if (!entry.isFile()) continue;
|
|
114
|
+
const fullPath = join(entry.parentPath, entry.name);
|
|
115
|
+
if (shouldIgnore(fullPath)) continue;
|
|
116
|
+
|
|
117
|
+
let mtime: number;
|
|
118
|
+
try {
|
|
119
|
+
const stats = await stat(fullPath);
|
|
120
|
+
mtime = stats.mtimeMs;
|
|
121
|
+
} catch {
|
|
122
|
+
mtime = Date.now();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (mtime < since) continue;
|
|
126
|
+
|
|
127
|
+
const metadata = await inferMetadata(fullPath);
|
|
128
|
+
const filepath = toPosix(fullPath.replace(cwd, ""));
|
|
129
|
+
yield {
|
|
130
|
+
type: "fs-sync",
|
|
131
|
+
detail: { metadata, filepath, timestamp: mtime },
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
} catch {
|
|
135
|
+
// .deco dir might not exist yet
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
yield {
|
|
139
|
+
type: "fs-snapshot",
|
|
140
|
+
detail: { timestamp: Date.now() },
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
// SSE handler — Connect-style middleware
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
|
|
148
|
+
export function createWatchHandler(opts?: { getPort?: () => number }) {
|
|
149
|
+
const cwd = process.cwd();
|
|
150
|
+
const getPort = opts?.getPort ?? (() => 5173);
|
|
151
|
+
|
|
152
|
+
return async (
|
|
153
|
+
req: IncomingMessage,
|
|
154
|
+
res: ServerResponse,
|
|
155
|
+
next: () => void,
|
|
156
|
+
): Promise<void> => {
|
|
157
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
158
|
+
|
|
159
|
+
// Only handle /watch or the root SSE endpoint
|
|
160
|
+
if (url.pathname !== "/watch" && url.pathname !== "/") {
|
|
161
|
+
next();
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (req.method !== "GET") {
|
|
166
|
+
next();
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// SSE headers
|
|
171
|
+
res.writeHead(200, {
|
|
172
|
+
"Content-Type": "text/event-stream",
|
|
173
|
+
"Cache-Control": "no-cache",
|
|
174
|
+
Connection: "keep-alive",
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
const since = Number(url.searchParams.get("since")) || 0;
|
|
178
|
+
let closed = false;
|
|
179
|
+
|
|
180
|
+
req.on("close", () => {
|
|
181
|
+
closed = true;
|
|
182
|
+
console.log("[deco] SSE stream closed");
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
function sendEvent(event: FSEvent): void {
|
|
186
|
+
if (closed) return;
|
|
187
|
+
const data = encodeURIComponent(JSON.stringify(event));
|
|
188
|
+
res.write(`event: message\ndata: ${data}\n\n`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Live broadcast listener
|
|
192
|
+
const handler = (e: Event) => {
|
|
193
|
+
const ce = e as CustomEvent<FSEvent>;
|
|
194
|
+
sendEvent(ce.detail);
|
|
195
|
+
};
|
|
196
|
+
channel.addEventListener("broadcast", handler);
|
|
197
|
+
req.on("close", () => {
|
|
198
|
+
channel.removeEventListener("broadcast", handler);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
console.log("[deco] SSE stream opened");
|
|
202
|
+
|
|
203
|
+
// Initial scan
|
|
204
|
+
for await (const event of scanFiles(cwd, since)) {
|
|
205
|
+
if (closed) break;
|
|
206
|
+
sendEvent(event);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (closed) return;
|
|
210
|
+
|
|
211
|
+
// Worker status — Vite dev server is always ready
|
|
212
|
+
sendEvent({
|
|
213
|
+
type: "worker-status",
|
|
214
|
+
detail: { state: "ready" },
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// Meta info — schema + manifest so admin knows about sections/loaders/actions.
|
|
218
|
+
// Fetch via HTTP so the request goes through Vite SSR where the data lives
|
|
219
|
+
// (daemon's native imports create separate module instances).
|
|
220
|
+
try {
|
|
221
|
+
const metaResponse = await fetch(`http://localhost:${getPort()}/live/_meta`);
|
|
222
|
+
if (metaResponse.ok) {
|
|
223
|
+
const metaData = await metaResponse.json();
|
|
224
|
+
sendEvent({
|
|
225
|
+
type: "meta-info",
|
|
226
|
+
detail: { ...metaData, timestamp: Date.now() },
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
} catch {
|
|
230
|
+
// Schema may not be initialized yet — admin will retry via /live/_meta
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ---------------------------------------------------------------------------
|
|
236
|
+
// Wire Vite watcher to broadcast channel
|
|
237
|
+
// ---------------------------------------------------------------------------
|
|
238
|
+
|
|
239
|
+
export function watchFS(watcher: {
|
|
240
|
+
on(event: string, cb: (...args: unknown[]) => void): void;
|
|
241
|
+
}): void {
|
|
242
|
+
const cwd = process.cwd();
|
|
243
|
+
|
|
244
|
+
const onChange = async (filePath: unknown, deleted = false) => {
|
|
245
|
+
if (typeof filePath !== "string") return;
|
|
246
|
+
if (shouldIgnore(filePath)) return;
|
|
247
|
+
|
|
248
|
+
const metadata = deleted ? null : await inferMetadata(filePath);
|
|
249
|
+
let mtime = Date.now();
|
|
250
|
+
if (!deleted) {
|
|
251
|
+
try {
|
|
252
|
+
const stats = await stat(filePath);
|
|
253
|
+
mtime = stats.mtimeMs;
|
|
254
|
+
} catch {
|
|
255
|
+
// use Date.now()
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
broadcastFSEvent({
|
|
260
|
+
type: "fs-sync",
|
|
261
|
+
detail: {
|
|
262
|
+
metadata,
|
|
263
|
+
filepath: toPosix(filePath.replace(cwd, "")),
|
|
264
|
+
timestamp: mtime,
|
|
265
|
+
},
|
|
266
|
+
});
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
watcher.on("change", (path: unknown) => onChange(path));
|
|
270
|
+
watcher.on("add", (path: unknown) => onChange(path));
|
|
271
|
+
watcher.on("unlink", (path: unknown) => onChange(path, true));
|
|
272
|
+
}
|