@decocms/start 5.3.0-rc.2 → 5.4.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/.github/workflows/release.yml +5 -2
- package/MIGRATION_TOOLING_PLAN.md +1 -0
- package/docs/observability.md +42 -5
- package/docs/tail-worker-recipe.md +161 -0
- package/package.json +1 -1
- package/scripts/audit-observability-config.ts +0 -0
- package/scripts/generate-blocks.ts +148 -71
- package/scripts/migrate-post-cleanup.ts +0 -0
- package/src/admin/decofile.ts +17 -8
- package/src/sdk/env.ts +7 -3
- package/src/sdk/workerEntry.ts +11 -7
- package/src/vite/plugin.js +129 -10
- package/docs/o11y.md +0 -602
- package/scripts/sync-decofile.ts +0 -221
package/src/sdk/workerEntry.ts
CHANGED
|
@@ -49,6 +49,7 @@ import { RequestContext } from "./requestContext";
|
|
|
49
49
|
import { cleanPathForCacheKey } from "./urlUtils";
|
|
50
50
|
import { type Device, isMobileUA } from "./useDevice";
|
|
51
51
|
import { getAppMiddleware } from "./setupApps";
|
|
52
|
+
import { isDevMode } from "./env";
|
|
52
53
|
|
|
53
54
|
/**
|
|
54
55
|
* Build-time identifier injected by `decoVitePlugin()` (see
|
|
@@ -802,8 +803,9 @@ export function createDecoWorkerEntry(
|
|
|
802
803
|
|
|
803
804
|
const geoVariants = body.countries ?? [];
|
|
804
805
|
|
|
805
|
-
const cache =
|
|
806
|
-
|
|
806
|
+
const cache = isDevMode()
|
|
807
|
+
? null
|
|
808
|
+
: typeof caches !== "undefined"
|
|
807
809
|
? ((caches as unknown as { default?: Cache }).default ?? null)
|
|
808
810
|
: null;
|
|
809
811
|
|
|
@@ -1200,8 +1202,9 @@ export function createDecoWorkerEntry(
|
|
|
1200
1202
|
request.method === "POST" &&
|
|
1201
1203
|
(url.pathname.startsWith("/_serverFn/") || url.pathname.startsWith("/_server/"))
|
|
1202
1204
|
) {
|
|
1203
|
-
const serverFnCache =
|
|
1204
|
-
|
|
1205
|
+
const serverFnCache = isDevMode()
|
|
1206
|
+
? null
|
|
1207
|
+
: typeof caches !== "undefined"
|
|
1205
1208
|
? ((caches as unknown as { default?: Cache }).default ?? null)
|
|
1206
1209
|
: null;
|
|
1207
1210
|
|
|
@@ -1422,9 +1425,10 @@ export function createDecoWorkerEntry(
|
|
|
1422
1425
|
return resp;
|
|
1423
1426
|
}
|
|
1424
1427
|
|
|
1425
|
-
// Check Cache API
|
|
1426
|
-
const cache =
|
|
1427
|
-
|
|
1428
|
+
// Check Cache API — disabled in local dev to avoid stale responses
|
|
1429
|
+
const cache = isDevMode()
|
|
1430
|
+
? null
|
|
1431
|
+
: typeof caches !== "undefined"
|
|
1428
1432
|
? ((caches as unknown as { default?: Cache }).default ?? null)
|
|
1429
1433
|
: null;
|
|
1430
1434
|
|
package/src/vite/plugin.js
CHANGED
|
@@ -32,7 +32,8 @@
|
|
|
32
32
|
* ```
|
|
33
33
|
*/
|
|
34
34
|
import { execFileSync } from "node:child_process";
|
|
35
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
35
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
36
|
+
import path from "node:path";
|
|
36
37
|
|
|
37
38
|
/**
|
|
38
39
|
* Resolve a per-build identifier for cache-key versioning.
|
|
@@ -197,17 +198,135 @@ export function decoVitePlugin() {
|
|
|
197
198
|
},
|
|
198
199
|
|
|
199
200
|
configureServer(server) {
|
|
200
|
-
//
|
|
201
|
-
//
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
201
|
+
// Watch `.deco/blocks/**/*.json` and regenerate `blocks.gen.json` when
|
|
202
|
+
// CMS content changes (manual edit, sync-decofile, daemon PATCH).
|
|
203
|
+
// After regen, we POST the new blocks to the dev server's own
|
|
204
|
+
// /.decofile endpoint — this calls setBlocks() inside the workerd SSR
|
|
205
|
+
// runtime without any module invalidation (which breaks TanStack
|
|
206
|
+
// Start/Router state).
|
|
207
|
+
//
|
|
208
|
+
// Generator is loaded lazily via tsImport (same pattern as the daemon
|
|
209
|
+
// below) so we don't depend on the consumer's TS loader.
|
|
210
|
+
const cwd = process.cwd();
|
|
211
|
+
const blocksDir = path.resolve(cwd, ".deco/blocks");
|
|
212
|
+
const outFile = path.resolve(cwd, "src/server/cms/blocks.gen.ts");
|
|
213
|
+
const jsonFile = outFile.replace(/\.ts$/, ".json");
|
|
214
|
+
|
|
215
|
+
let generateBlocksFn;
|
|
216
|
+
const loadGenerator = () => {
|
|
217
|
+
if (generateBlocksFn) return Promise.resolve(generateBlocksFn);
|
|
218
|
+
// Same tsImport pattern as the daemon loader below — keeps `tsx`
|
|
219
|
+
// scoped to this single import instead of registering a global hook.
|
|
220
|
+
return import("tsx/esm/api")
|
|
221
|
+
.then(({ tsImport }) =>
|
|
222
|
+
tsImport("../../scripts/generate-blocks.ts", import.meta.url),
|
|
223
|
+
)
|
|
224
|
+
.then((mod) => {
|
|
225
|
+
generateBlocksFn = mod.generateBlocks;
|
|
226
|
+
return generateBlocksFn;
|
|
227
|
+
});
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
let regenTimer = null;
|
|
231
|
+
let regenInFlight = false;
|
|
232
|
+
let regenQueued = false;
|
|
233
|
+
const runRegen = async () => {
|
|
234
|
+
if (regenInFlight) {
|
|
235
|
+
regenQueued = true;
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
regenInFlight = true;
|
|
239
|
+
try {
|
|
240
|
+
const fn = await loadGenerator();
|
|
241
|
+
const start = Date.now();
|
|
242
|
+
const result = await fn({ blocksDir, outFile, silent: true });
|
|
243
|
+
const ms = Date.now() - start;
|
|
244
|
+
if (result.empty) {
|
|
245
|
+
console.warn(`[deco] .deco/blocks not found — emitted empty blocks.gen.json`);
|
|
246
|
+
} else {
|
|
247
|
+
console.log(`[deco] regenerated ${result.count} blocks in ${ms}ms`);
|
|
248
|
+
// POST blocks to the dev server's /.decofile endpoint so
|
|
249
|
+
// setBlocks() runs inside the workerd SSR runtime. No module
|
|
250
|
+
// invalidation — that would cascade through the route tree and
|
|
251
|
+
// break TanStack Start server functions.
|
|
252
|
+
try {
|
|
253
|
+
const addr = server.httpServer?.address();
|
|
254
|
+
const port = typeof addr === "object" && addr ? addr.port : 5173;
|
|
255
|
+
const blocksJson = readFileSync(jsonFile, "utf-8");
|
|
256
|
+
const res = await fetch(`http://localhost:${port}/.decofile`, {
|
|
257
|
+
method: "POST",
|
|
258
|
+
headers: { "Content-Type": "application/json" },
|
|
259
|
+
body: blocksJson,
|
|
260
|
+
});
|
|
261
|
+
if (res.ok) {
|
|
262
|
+
server.hot?.send({ type: "full-reload", path: "*" });
|
|
263
|
+
} else {
|
|
264
|
+
console.warn(`[deco] blocks reload failed: ${res.status}`);
|
|
265
|
+
}
|
|
266
|
+
} catch (reloadErr) {
|
|
267
|
+
console.warn("[deco] blocks reload request failed:", reloadErr?.message);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
} catch (err) {
|
|
271
|
+
console.warn("[deco] failed to regenerate blocks:", err?.message ?? err);
|
|
272
|
+
} finally {
|
|
273
|
+
regenInFlight = false;
|
|
274
|
+
if (regenQueued) {
|
|
275
|
+
regenQueued = false;
|
|
276
|
+
scheduleRegen();
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
const scheduleRegen = () => {
|
|
281
|
+
if (regenTimer) clearTimeout(regenTimer);
|
|
282
|
+
regenTimer = setTimeout(() => {
|
|
283
|
+
regenTimer = null;
|
|
284
|
+
runRegen();
|
|
285
|
+
}, 150);
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
// chokidar (Vite's watcher) needs the directory added explicitly because
|
|
289
|
+
// `.deco/` lives outside the module graph it walks by default.
|
|
290
|
+
if (existsSync(blocksDir)) {
|
|
291
|
+
server.watcher.add(blocksDir);
|
|
292
|
+
}
|
|
293
|
+
const handleBlocksDirEvent = (file) => {
|
|
294
|
+
if (!file.endsWith(".json")) return;
|
|
295
|
+
if (!file.startsWith(blocksDir + path.sep) && file !== blocksDir) return;
|
|
296
|
+
scheduleRegen();
|
|
297
|
+
};
|
|
298
|
+
server.watcher.on("add", handleBlocksDirEvent);
|
|
299
|
+
server.watcher.on("change", handleBlocksDirEvent);
|
|
300
|
+
server.watcher.on("unlink", handleBlocksDirEvent);
|
|
301
|
+
|
|
302
|
+
// Cold-start bootstrap: regenerate once if the artifact is missing or
|
|
303
|
+
// older than the newest source file. Skips work on a clean build where
|
|
304
|
+
// `npm run build` already produced a current artifact.
|
|
305
|
+
try {
|
|
306
|
+
const needsBootstrap = (() => {
|
|
307
|
+
if (!existsSync(jsonFile)) return existsSync(blocksDir);
|
|
308
|
+
if (!existsSync(blocksDir)) return false;
|
|
309
|
+
const artifactMtime = statSync(jsonFile).mtimeMs;
|
|
310
|
+
for (const entry of readdirSync(blocksDir)) {
|
|
311
|
+
if (!entry.endsWith(".json")) continue;
|
|
312
|
+
try {
|
|
313
|
+
if (statSync(path.join(blocksDir, entry)).mtimeMs > artifactMtime) {
|
|
314
|
+
return true;
|
|
315
|
+
}
|
|
316
|
+
} catch {
|
|
317
|
+
// skip unreadable entries
|
|
318
|
+
}
|
|
208
319
|
}
|
|
320
|
+
return false;
|
|
321
|
+
})();
|
|
322
|
+
if (needsBootstrap) {
|
|
323
|
+
// Fire and forget — the next request that touches blocks.gen.ts
|
|
324
|
+
// will see the fresh artifact thanks to the change listener above.
|
|
325
|
+
runRegen();
|
|
209
326
|
}
|
|
210
|
-
})
|
|
327
|
+
} catch (err) {
|
|
328
|
+
console.warn("[deco] blocks bootstrap check failed:", err?.message ?? err);
|
|
329
|
+
}
|
|
211
330
|
|
|
212
331
|
// Tunnel + daemon: connect local dev to admin.deco.cx
|
|
213
332
|
// Activated only when both DECO_SITE_NAME and DECO_ENV_NAME are set.
|